mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-11 00:50:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f73cfe60 | ||
|
|
032e188a24 | ||
|
|
2dbf8de089 | ||
|
|
1c0459683b | ||
|
|
856a8403b3 | ||
|
|
dc6209dbc1 | ||
|
|
4b540faec4 | ||
|
|
2f6c95dd51 | ||
|
|
beb77d3fe9 | ||
|
|
152f137cad | ||
|
|
1358cb0e6d | ||
|
|
ca54b450de | ||
|
|
2221e6a29c | ||
|
|
db44823ecf | ||
|
|
66623693df | ||
|
|
e2ab2eb603 | ||
|
|
87bdfc5e91 | ||
|
|
78fe589fcf | ||
|
|
bcd09af2d6 | ||
|
|
de97ad6b50 | ||
|
|
417841b3d2 | ||
|
|
11f8631929 | ||
|
|
d650247817 | ||
|
|
46df27e8c6 | ||
|
|
bb75bc9400 | ||
|
|
8acdfc216d | ||
|
|
16e7b7e6e5 | ||
|
|
0f46b20fc4 | ||
|
|
3654161245 | ||
|
|
d691fc837e |
@@ -6,6 +6,12 @@
|
||||
|
||||
GoMFT is a web-based managed file transfer application built with Go, leveraging rclone for robust file transfer capabilities. It provides a user-friendly interface for configuring, scheduling, and monitoring file transfers across various storage providers.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/f9dwtM3j">
|
||||
<img src="https://img.shields.io/discord/1351354052654403675?color=7289da&logo=discord&logoColor=white&label=Discord" alt="Join our Discord server!" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
> This application is actively under development. As such, any aspect of the application—including configurations, data structures, and database fields—may change rapidly and without prior notice. Please review all release notes thoroughly before updating.
|
||||
|
||||
@@ -44,6 +50,9 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
|
||||
- SFTP
|
||||
- FTP
|
||||
- SMB/CIFS shares
|
||||
- Hetzner Storage Box
|
||||
- Backblaze B2
|
||||
- Wasabi
|
||||
- Local filesystem
|
||||
- And more via rclone
|
||||
- **Webhook Notifications**: Receive real-time notifications of job events:
|
||||
@@ -360,8 +369,13 @@ Log files contain detailed information about file transfers, job execution, and
|
||||
5. Create jobs using your configurations:
|
||||
- Navigate to "Jobs" section
|
||||
- Select an existing transfer config
|
||||
- Set up a schedule using cron expressions or run manually
|
||||
- Enable/disable as needed
|
||||
- Use the visual schedule builder to set your timing preferences:
|
||||
- Choose from common presets (hourly, daily, weekly, monthly)
|
||||
- Customize with specific days, times, or intervals
|
||||
- See a plain-language description of your schedule
|
||||
- View upcoming run times on the interactive calendar
|
||||
- Switch to advanced mode for direct cron expression input if needed
|
||||
- Enable/disable jobs with a single click
|
||||
|
||||
6. Monitor transfers:
|
||||
- View active and completed transfers on the Dashboard
|
||||
@@ -525,6 +539,10 @@ The following fields have been added to the `users` table:
|
||||
- Local filesystem
|
||||
- Amazon S3
|
||||
- MinIO (S3-compatible storage)
|
||||
- NextCloud
|
||||
- Backblaze B2
|
||||
- Wasabi
|
||||
- Hetzner Storage Box
|
||||
- SFTP
|
||||
- FTP
|
||||
- SMB/CIFS shares
|
||||
@@ -568,9 +586,14 @@ The following fields have been added to the `users` table:
|
||||
- Adaptive processing based on source/destination capabilities
|
||||
|
||||
7. **Schedule Options**:
|
||||
- Cron expressions for flexible scheduling
|
||||
- Manual execution
|
||||
- Enable/disable schedules
|
||||
- **Visual Schedule Builder**: Intuitive interface for setting schedule preferences
|
||||
- **Natural Language Description**: Plain-language description of schedule
|
||||
- **Interactive Calendar**: Visual representation of upcoming runs
|
||||
- **Common Presets**: Hourly, daily, weekly, monthly schedules
|
||||
- **Advanced Mode**: Manual cron expression input for complex schedules
|
||||
- **Schedule Validation**: Preview and confirm schedule
|
||||
- **Enable/Disable**: One-click enable/disable
|
||||
- **Time Zone Support**: Accurate scheduling based on user's time zone
|
||||
|
||||
8. **Notification Options**:
|
||||
- **Email Notifications**: Receive job status updates via email
|
||||
@@ -764,92 +787,4 @@ volumes:
|
||||
- /host/path/backups:/app/backups # For database backups
|
||||
```
|
||||
|
||||
These paths can be customized using the environment variables `DATA_DIR`, `BACKUP_DIR`, and `LOGS_DIR`.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Running as a Non-Root User
|
||||
|
||||
By default, Docker containers run as the root user, which can pose security risks. GoMFT supports running as a non-root user, which is recommended for production environments.
|
||||
|
||||
#### Benefits of Running as Non-Root
|
||||
|
||||
- **Improved Security**: Limits the potential damage if the container is compromised
|
||||
- **Better File Permissions**: Files created by the container will match your host user permissions
|
||||
- **Compliance**: Many security policies and best practices require containers to run as non-root
|
||||
|
||||
#### Methods to Run as Non-Root
|
||||
|
||||
1. **Using PUID/PGID environment variables (recommended)**:
|
||||
```bash
|
||||
# Using current user's ID
|
||||
docker run -e PUID=$(id -u) -e PGID=$(id -g) starfleetcptn/gomft:latest
|
||||
|
||||
# Or in docker-compose.yml
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
```
|
||||
This is the most flexible method as it allows changing the user at runtime without rebuilding the image.
|
||||
|
||||
2. **Using the `--user` flag with Docker run**:
|
||||
```bash
|
||||
docker run --user $(id -u):$(id -g) starfleetcptn/gomft:latest
|
||||
```
|
||||
|
||||
3. **Using Docker Compose with environment variables for `user` directive**:
|
||||
```yaml
|
||||
services:
|
||||
gomft:
|
||||
image: starfleetcptn/gomft:latest
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
```
|
||||
|
||||
4. **Building a custom image with specified UID/GID**:
|
||||
```yaml
|
||||
services:
|
||||
gomft:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
UID: ${UID:-1000}
|
||||
GID: ${GID:-1000}
|
||||
```
|
||||
|
||||
#### Environment Variables for User Management
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PUID` | User ID to run as | Built-in user ID (1000) |
|
||||
| `PGID` | Group ID to run as | Built-in group ID (1000) |
|
||||
| `USERNAME` | Username to use | `gomft` |
|
||||
|
||||
These environment variables allow you to change the user/group IDs at runtime without rebuilding the image.
|
||||
|
||||
#### Volume Permissions
|
||||
|
||||
When mounting volumes, ensure that the directories on the host have appropriate permissions for the container user:
|
||||
|
||||
```bash
|
||||
# Create directories with correct ownership
|
||||
mkdir -p data backups
|
||||
chown -R $(id -u):$(id -g) data backups
|
||||
|
||||
# Or adjust permissions to allow the container user to write
|
||||
mkdir -p data backups
|
||||
chmod -R 777 data backups # Less secure, but easier for testing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE) - see the full license terms
|
||||
|
||||
The GoMFT logo is licensed under the Creative Commons Attribution 4.0 International Public License.
|
||||
|
||||
The gopher design is from https://github.com/egonelbre/gophers.
|
||||
|
||||
The original Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
|
||||
These paths can be customized using the environment variables `
|
||||
@@ -0,0 +1,538 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogEntry represents a log entry for display
|
||||
type LogEntry struct {
|
||||
Timestamp time.Time
|
||||
Level string
|
||||
Message string
|
||||
Source string
|
||||
Details map[string]interface{}
|
||||
}
|
||||
|
||||
// LogViewerData represents the data for the log viewer component
|
||||
type LogViewerData struct {
|
||||
Logs []LogEntry
|
||||
CurrentFilter string
|
||||
LogFilePath string
|
||||
}
|
||||
|
||||
// Helper function to get the appropriate CSS class for log levels
|
||||
func getLogLevelClass(level string) string {
|
||||
baseClass := "px-4 py-2 text-sm font-medium whitespace-nowrap "
|
||||
|
||||
switch level {
|
||||
case "debug":
|
||||
return baseClass + "text-purple-500 dark:text-purple-400"
|
||||
case "info":
|
||||
return baseClass + "text-blue-500 dark:text-blue-400"
|
||||
case "warn":
|
||||
return baseClass + "text-yellow-500 dark:text-yellow-400"
|
||||
case "error":
|
||||
return baseClass + "text-red-500 dark:text-red-400"
|
||||
case "fatal":
|
||||
return baseClass + "text-red-700 dark:text-red-600 font-bold"
|
||||
default:
|
||||
return baseClass + "text-gray-500 dark:text-gray-400"
|
||||
}
|
||||
}
|
||||
|
||||
// AdminLogs renders the log viewer page
|
||||
templ AdminLogs(ctx context.Context, data LogViewerData) {
|
||||
@LayoutWithContext("Log Viewer", ctx) {
|
||||
<div class="log-viewer-page">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-stream w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i> Log Viewer
|
||||
</h1>
|
||||
<div class="flex gap-2">
|
||||
<button id="pause-logs" class="flex items-center justify-center text-white bg-yellow-500 hover:bg-yellow-600 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg px-4 py-2 dark:bg-yellow-600 dark:hover:bg-yellow-700 focus:outline-none dark:focus:ring-yellow-800">
|
||||
<i class="fas fa-pause w-4 h-4 mr-2"></i> Pause
|
||||
</button>
|
||||
<button id="resume-logs" class="hidden flex items-center justify-center text-white bg-green-500 hover:bg-green-600 focus:ring-4 focus:ring-green-300 font-medium rounded-lg px-4 py-2 dark:bg-green-600 dark:hover:bg-green-700 focus:outline-none dark:focus:ring-green-800">
|
||||
<i class="fas fa-play w-4 h-4 mr-2"></i> Resume
|
||||
</button>
|
||||
<button id="clear-logs" class="flex items-center justify-center text-white bg-red-500 hover:bg-red-600 focus:ring-4 focus:ring-red-300 font-medium rounded-lg px-4 py-2 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||
<i class="fas fa-trash w-4 h-4 mr-2"></i> Clear
|
||||
</button>
|
||||
<button id="download-logs" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-download w-4 h-4 mr-2"></i> Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Information -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 mb-4 p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
<p><i class="fas fa-info-circle mr-2 text-blue-500 dark:text-blue-400"></i> Viewing logs from: <span class="font-mono">{ data.LogFilePath }</span></p>
|
||||
<p><i class="fas fa-circle text-green-500 dark:text-green-400 mr-2"></i> Real-time log streaming is active, logs are automatically captured and displayed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 mb-6">
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Filter Logs</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<!-- Log Level Filter -->
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="filter-level" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Log Level</label>
|
||||
<select id="filter-level" 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 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">
|
||||
<option value="">All Levels</option>
|
||||
<option value="debug">Debug</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warn">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="fatal">Fatal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Source Filter -->
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="filter-source" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Source</label>
|
||||
<select id="filter-source" 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 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">
|
||||
<option value="">All Sources</option>
|
||||
<option value="api">API</option>
|
||||
<option value="web">Web</option>
|
||||
<option value="scheduler">Scheduler</option>
|
||||
<option value="auth">Authentication</option>
|
||||
<option value="database">Database</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Search Filter -->
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="filter-search" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Search</label>
|
||||
<input type="text" id="filter-search" 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 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="Search logs...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Table -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Live Logs</h3>
|
||||
<div class="flex items-center">
|
||||
<span id="connection-status" class="flex items-center text-sm text-green-500 dark:text-green-400">
|
||||
<span class="inline-block w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full mr-2"></span>
|
||||
Connected
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto" style="max-height: 60vh; overflow-y: auto;">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Timestamp</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase w-[100px]">Level</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase w-[120px]">Source</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="log-entries" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- Log entries will be inserted here dynamically -->
|
||||
if len(data.Logs) == 0 {
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Waiting for logs...</td>
|
||||
</tr>
|
||||
} else {
|
||||
for _, log := range data.Logs {
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{ log.Timestamp.Format("2006-01-02 15:04:05.000") }</td>
|
||||
<td class={ getLogLevelClass(log.Level) }>{ log.Level }</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{ log.Source }</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-900 dark:text-white font-mono">{ log.Message }</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const logEntries = document.getElementById('log-entries');
|
||||
const pauseButton = document.getElementById('pause-logs');
|
||||
const resumeButton = document.getElementById('resume-logs');
|
||||
const clearButton = document.getElementById('clear-logs');
|
||||
const downloadButton = document.getElementById('download-logs');
|
||||
const connectionStatus = document.getElementById('connection-status');
|
||||
const filterLevel = document.getElementById('filter-level');
|
||||
const filterSource = document.getElementById('filter-source');
|
||||
const filterSearch = document.getElementById('filter-search');
|
||||
|
||||
let isPaused = false;
|
||||
let logs = [];
|
||||
let filteredLogs = [];
|
||||
let ws;
|
||||
let knownSources = new Set();
|
||||
let reconnectTimer = null;
|
||||
let pingInterval = null;
|
||||
|
||||
// Connect to WebSocket
|
||||
function connectWebSocket() {
|
||||
// Clear any existing reconnect timer
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
|
||||
// Clear any existing ping interval
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
pingInterval = null;
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = protocol + '//' + window.location.host + '/admin/logs/ws';
|
||||
|
||||
console.log("Connecting to WebSocket:", wsUrl);
|
||||
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-yellow-500 dark:bg-yellow-400 rounded-full mr-2"></span>Connecting...';
|
||||
connectionStatus.className = 'flex items-center text-sm text-yellow-500 dark:text-yellow-400';
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log("WebSocket connection established");
|
||||
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full mr-2"></span>Connected';
|
||||
connectionStatus.className = 'flex items-center text-sm text-green-500 dark:text-green-400';
|
||||
|
||||
// Set up ping interval to keep connection alive
|
||||
pingInterval = setInterval(function() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
console.debug("Sending ping to server");
|
||||
// Send a simple ping message
|
||||
ws.send(JSON.stringify({type: "ping"}));
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
};
|
||||
|
||||
ws.onclose = function(event) {
|
||||
console.log("WebSocket connection closed", event);
|
||||
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></span>Disconnected';
|
||||
connectionStatus.className = 'flex items-center text-sm text-red-500 dark:text-red-400';
|
||||
|
||||
// Clear the ping interval
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
pingInterval = null;
|
||||
}
|
||||
|
||||
// Attempt to reconnect after 5 seconds
|
||||
console.log("Scheduling reconnect in 5 seconds...");
|
||||
reconnectTimer = setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error("WebSocket error:", error);
|
||||
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></span>Error';
|
||||
connectionStatus.className = 'flex items-center text-sm text-red-500 dark:text-red-400';
|
||||
|
||||
// Don't set up reconnect here, let onclose handle it
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
// Debug log the received data
|
||||
console.debug("Raw log entry received:", event.data);
|
||||
|
||||
try {
|
||||
const logEntry = JSON.parse(event.data);
|
||||
|
||||
// Debug log the parsed entry
|
||||
console.debug("Parsed log entry:", logEntry);
|
||||
|
||||
// Extract source and add to known sources for filtering
|
||||
const source = logEntry.Source || logEntry.source || '';
|
||||
if (source && !knownSources.has(source)) {
|
||||
knownSources.add(source);
|
||||
updateSourceFilter();
|
||||
}
|
||||
|
||||
// Handle potential log prefixes in the message
|
||||
const message = logEntry.Message || logEntry.message || '';
|
||||
if (message.startsWith("DEBUG:")) {
|
||||
logEntry.Level = "debug";
|
||||
logEntry.Message = message.substring(7).trim();
|
||||
} else if (message.startsWith("INFO:")) {
|
||||
logEntry.Level = "info";
|
||||
logEntry.Message = message.substring(6).trim();
|
||||
} else if (message.startsWith("ERROR:")) {
|
||||
logEntry.Level = "error";
|
||||
logEntry.Message = message.substring(7).trim();
|
||||
} else if (message.startsWith("WARN:")) {
|
||||
logEntry.Level = "warn";
|
||||
logEntry.Message = message.substring(6).trim();
|
||||
} else if (message.startsWith("WARNING:")) {
|
||||
logEntry.Level = "warn";
|
||||
logEntry.Message = message.substring(9).trim();
|
||||
} else if (message.startsWith("FATAL:")) {
|
||||
logEntry.Level = "fatal";
|
||||
logEntry.Message = message.substring(7).trim();
|
||||
}
|
||||
|
||||
// Add to logs array
|
||||
logs.push(logEntry);
|
||||
|
||||
// Apply filters and update display if not paused
|
||||
if (!isPaused) {
|
||||
applyFilters();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing log entry:", error);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating WebSocket:", error);
|
||||
connectionStatus.innerHTML = '<span class="inline-block w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></span>Connection Failed';
|
||||
connectionStatus.className = 'flex items-center text-sm text-red-500 dark:text-red-400';
|
||||
|
||||
// Retry connection after 5 seconds
|
||||
reconnectTimer = setTimeout(connectWebSocket, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the source filter dropdown with dynamically discovered sources
|
||||
function updateSourceFilter() {
|
||||
// Remember the current selection
|
||||
const currentValue = filterSource.value;
|
||||
|
||||
// Clear existing options except the first "All Sources" option
|
||||
while (filterSource.options.length > 1) {
|
||||
filterSource.remove(1);
|
||||
}
|
||||
|
||||
// Add sorted sources to dropdown
|
||||
Array.from(knownSources).sort().forEach(source => {
|
||||
const option = document.createElement('option');
|
||||
option.value = source.toLowerCase();
|
||||
option.textContent = source;
|
||||
filterSource.appendChild(option);
|
||||
});
|
||||
|
||||
// Restore previous selection if it still exists
|
||||
if (currentValue) {
|
||||
for (let i = 0; i < filterSource.options.length; i++) {
|
||||
if (filterSource.options[i].value === currentValue) {
|
||||
filterSource.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters to logs
|
||||
function applyFilters() {
|
||||
const levelFilter = filterLevel.value.toLowerCase();
|
||||
const sourceFilter = filterSource.value.toLowerCase();
|
||||
const searchFilter = filterSearch.value.toLowerCase();
|
||||
|
||||
filteredLogs = logs.filter(log => {
|
||||
// Handle capitalized properties from the server
|
||||
const level = (log.Level || log.level || '').toLowerCase();
|
||||
const source = (log.Source || log.source || '').toLowerCase();
|
||||
const message = (log.Message || log.message || '').toLowerCase();
|
||||
|
||||
return (levelFilter === '' || level === levelFilter) &&
|
||||
(sourceFilter === '' || source === sourceFilter) &&
|
||||
(searchFilter === '' || message.includes(searchFilter));
|
||||
});
|
||||
|
||||
renderLogs();
|
||||
}
|
||||
|
||||
// Render logs to the table
|
||||
function renderLogs() {
|
||||
// Clear existing logs
|
||||
logEntries.innerHTML = '';
|
||||
|
||||
if (filteredLogs.length === 0) {
|
||||
const emptyRow = document.createElement('tr');
|
||||
emptyRow.innerHTML = `<td colspan="4" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">No logs found</td>`;
|
||||
logEntries.appendChild(emptyRow);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add filtered logs
|
||||
filteredLogs.forEach(log => {
|
||||
// Handle capitalized property names from the server
|
||||
const timestamp = log.Timestamp || log.timestamp;
|
||||
const level = log.Level || log.level || 'unknown';
|
||||
const source = log.Source || log.source || 'unknown';
|
||||
const message = log.Message || log.message || '';
|
||||
|
||||
let formattedTime;
|
||||
try {
|
||||
// Convert to date object
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// Format in local time with milliseconds
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
|
||||
// Format main part of the timestamp
|
||||
formattedTime = date.toLocaleString(undefined, options);
|
||||
|
||||
// Add milliseconds
|
||||
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||
formattedTime += "." + ms;
|
||||
} catch (e) {
|
||||
console.error("Error formatting timestamp:", e);
|
||||
formattedTime = String(timestamp);
|
||||
}
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||
|
||||
let levelClass = 'px-4 py-2 text-sm font-medium whitespace-nowrap ';
|
||||
|
||||
switch(level.toLowerCase()) {
|
||||
case 'debug':
|
||||
levelClass += 'text-purple-500 dark:text-purple-400';
|
||||
break;
|
||||
case 'info':
|
||||
levelClass += 'text-blue-500 dark:text-blue-400';
|
||||
break;
|
||||
case 'warn':
|
||||
levelClass += 'text-yellow-500 dark:text-yellow-400';
|
||||
break;
|
||||
case 'error':
|
||||
levelClass += 'text-red-500 dark:text-red-400';
|
||||
break;
|
||||
case 'fatal':
|
||||
levelClass += 'text-red-700 dark:text-red-600 font-bold';
|
||||
break;
|
||||
default:
|
||||
levelClass += 'text-gray-500 dark:text-gray-400';
|
||||
}
|
||||
|
||||
row.innerHTML =
|
||||
'<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">' + formattedTime + '</td>' +
|
||||
'<td class="' + levelClass + '">' + level + '</td>' +
|
||||
'<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">' + source + '</td>' +
|
||||
'<td class="px-4 py-2 text-sm text-gray-900 dark:text-white font-mono">' + message + '</td>';
|
||||
|
||||
logEntries.appendChild(row);
|
||||
});
|
||||
|
||||
// Auto-scroll to bottom unless user has scrolled up
|
||||
const container = logEntries.parentElement;
|
||||
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 100) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Pause button click
|
||||
pauseButton.addEventListener('click', function() {
|
||||
isPaused = true;
|
||||
pauseButton.classList.add('hidden');
|
||||
resumeButton.classList.remove('hidden');
|
||||
});
|
||||
|
||||
// Resume button click
|
||||
resumeButton.addEventListener('click', function() {
|
||||
isPaused = false;
|
||||
resumeButton.classList.add('hidden');
|
||||
pauseButton.classList.remove('hidden');
|
||||
applyFilters(); // Re-apply filters and update
|
||||
});
|
||||
|
||||
// Clear button click
|
||||
clearButton.addEventListener('click', function() {
|
||||
logs = [];
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// Download button click
|
||||
downloadButton.addEventListener('click', function() {
|
||||
// Create CSV from logs
|
||||
let csv = 'Timestamp,Level,Source,Message\n';
|
||||
|
||||
logs.forEach(log => {
|
||||
// Handle capitalized property names from the server
|
||||
const timestamp = log.Timestamp || log.timestamp;
|
||||
const level = log.Level || log.level || 'unknown';
|
||||
const source = log.Source || log.source || 'unknown';
|
||||
const message = log.Message || log.message || '';
|
||||
|
||||
let formattedTime;
|
||||
try {
|
||||
// Convert to date object
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// Format in local time with milliseconds
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
|
||||
// Format main part of the timestamp
|
||||
formattedTime = date.toLocaleString(undefined, options);
|
||||
|
||||
// Add milliseconds
|
||||
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||
formattedTime += "." + ms;
|
||||
} catch (e) {
|
||||
formattedTime = String(timestamp);
|
||||
}
|
||||
|
||||
// Properly escape CSV fields
|
||||
let escapedMessage = '';
|
||||
if (message) {
|
||||
escapedMessage = message.split('"').join('""');
|
||||
}
|
||||
|
||||
csv += '"' + formattedTime + '","' + level + '","' + source + '","' + escapedMessage + '"\n';
|
||||
});
|
||||
|
||||
// Create and trigger download
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const date = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
||||
|
||||
a.setAttribute('href', url);
|
||||
a.setAttribute('download', 'gomft-logs-' + date + '.csv');
|
||||
a.click();
|
||||
});
|
||||
|
||||
// Filter change handlers
|
||||
filterLevel.addEventListener('change', applyFilters);
|
||||
filterSource.addEventListener('change', applyFilters);
|
||||
|
||||
// Debounce search input
|
||||
let searchTimeout;
|
||||
filterSearch.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(applyFilters, 300);
|
||||
});
|
||||
|
||||
// Initial connection
|
||||
connectWebSocket();
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
+278
-10
@@ -167,6 +167,9 @@ func getInitialData(config *db.TransferConfig) string {
|
||||
deleteAfterTransfer = config.GetDeleteAfterTransfer()
|
||||
skipProcessedFiles = config.GetSkipProcessedFiles()
|
||||
maxConcurrentTransfers = config.MaxConcurrentTransfers
|
||||
if maxConcurrentTransfers <= 0 {
|
||||
maxConcurrentTransfers = 1 // Ensure at least 1 concurrent transfer
|
||||
}
|
||||
rcloneFlags = config.RcloneFlags
|
||||
commandId = config.CommandID
|
||||
commandFlags = config.CommandFlags
|
||||
@@ -377,6 +380,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
|
||||
<!-- Main Form -->
|
||||
<form
|
||||
id="config-form"
|
||||
class="space-y-6"
|
||||
if data.IsNew {
|
||||
hx-post="/configs"
|
||||
@@ -398,6 +402,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
sourcePort = 22;
|
||||
} else if (sourceType === 'ftp') {
|
||||
sourcePort = 21;
|
||||
} else if (sourceType === 'hetzner') {
|
||||
sourcePort = 23;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,28 +412,50 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
destPort = 22;
|
||||
} else if (destinationType === 'ftp') {
|
||||
destPort = 21;
|
||||
} else if (destinationType === 'hetzner') {
|
||||
destPort = 23;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure maxConcurrentTransfers is at least 1
|
||||
if (!maxConcurrentTransfers || maxConcurrentTransfers < 1) {
|
||||
maxConcurrentTransfers = 1;
|
||||
}
|
||||
|
||||
// Initialize command requirements
|
||||
updateCommandRequirements();
|
||||
})"
|
||||
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21)) {
|
||||
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21 || sourcePort === 23)) {
|
||||
sourcePort = 22;
|
||||
console.log('Updating source port to 22 for SFTP');
|
||||
} else if (sourceType === 'ftp' && (sourcePort === 0 || sourcePort === 22)) {
|
||||
} else if (sourceType === 'ftp' && (sourcePort === 0 || sourcePort === 22 || sourcePort === 23)) {
|
||||
sourcePort = 21;
|
||||
console.log('Updating source port to 21 for FTP');
|
||||
}"
|
||||
x-effect="if (destinationType === 'sftp' && (destPort === 0 || destPort === 21)) {
|
||||
} else if (sourceType === 'hetzner' && (sourcePort === 0 || sourcePort === 22 || sourcePort === 21)) {
|
||||
sourcePort = 23;
|
||||
console.log('Updating source port to 23 for Hetzner');
|
||||
} else if (destinationType === 'sftp' && (destPort === 0 || destPort === 21 || destPort === 23)) {
|
||||
destPort = 22;
|
||||
console.log('Updating destination port to 22 for SFTP');
|
||||
} else if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) {
|
||||
} else if (destinationType === 'ftp' && (destPort === 0 || destPort === 22 || destPort === 23)) {
|
||||
destPort = 21;
|
||||
console.log('Updating destination port to 21 for FTP');
|
||||
} else if (destinationType === 'hetzner' && (destPort === 0 || destPort === 22 || destPort === 21)) {
|
||||
destPort = 23;
|
||||
console.log('Updating destination port to 23 for Hetzner');
|
||||
}"
|
||||
@formvalidation
|
||||
>
|
||||
|
||||
<!-- Form Error Container -->
|
||||
<div id="form-errors" class="hidden p-4 mb-6 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-800/20 dark:text-red-400 border border-red-200 dark:border-red-900" role="alert">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fas fa-exclamation-circle text-red-600 dark:text-red-500 mr-2"></i>
|
||||
<h3 class="text-base font-medium text-red-800 dark:text-red-400">Please correct the following errors:</h3>
|
||||
</div>
|
||||
<ul id="error-list" class="ml-5 list-disc space-y-1"></ul>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Details Section -->
|
||||
<div class="p-4 mb-4 bg-blue-50 border border-blue-100 rounded-lg dark:bg-blue-900/20 dark:border-blue-900">
|
||||
<div class="flex items-center mb-2">
|
||||
@@ -441,13 +469,16 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
|
||||
<!-- Name field -->
|
||||
<div class="mb-2">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Configuration Name</label>
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
Configuration Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
x-model="name"
|
||||
required
|
||||
aria-required="true"
|
||||
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 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 Transfer Configuration"
|
||||
/>
|
||||
@@ -509,15 +540,23 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
<template x-if="sourceType === 'ftp'">
|
||||
@source.FTPSourceForm()
|
||||
</template>
|
||||
|
||||
|
||||
<template x-if="sourceType === 's3'">
|
||||
@source.S3SourceForm()
|
||||
</template>
|
||||
|
||||
<template x-if="sourceType === 'b2'">
|
||||
@source.B2SourceForm()
|
||||
</template>
|
||||
|
||||
<template x-if="sourceType === 'wasabi'">
|
||||
@source.WasabiSourceForm()
|
||||
</template>
|
||||
|
||||
<template x-if="sourceType === 'minio'">
|
||||
@source.MinIOSourceForm()
|
||||
</template>
|
||||
|
||||
|
||||
<template x-if="sourceType === 'smb'">
|
||||
@source.SMBSourceForm()
|
||||
</template>
|
||||
@@ -525,7 +564,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
<template x-if="sourceType === 'webdav'">
|
||||
@source.WebDAVSourceForm()
|
||||
</template>
|
||||
|
||||
|
||||
<template x-if="sourceType === 'nextcloud'">
|
||||
@source.NextCloudSourceForm()
|
||||
</template>
|
||||
@@ -533,10 +572,14 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
<template x-if="sourceType === 'gdrive'">
|
||||
@source.GoogleDriveSourceForm()
|
||||
</template>
|
||||
|
||||
|
||||
<template x-if="sourceType === 'gphotos'">
|
||||
@source.GooglePhotosSourceForm()
|
||||
</template>
|
||||
|
||||
<template x-if="sourceType === 'hetzner'">
|
||||
@source.HetznerSourceForm()
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- File Pattern Section -->
|
||||
@@ -589,6 +632,14 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
@destination.S3DestinationForm()
|
||||
</template>
|
||||
|
||||
<template x-if="destinationType === 'b2'">
|
||||
@destination.B2DestinationForm()
|
||||
</template>
|
||||
|
||||
<template x-if="destinationType === 'wasabi'">
|
||||
@destination.WasabiDestinationForm()
|
||||
</template>
|
||||
|
||||
<template x-if="destinationType === 'minio'">
|
||||
@destination.MinIODestinationForm()
|
||||
</template>
|
||||
@@ -612,6 +663,10 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
<template x-if="destinationType === 'gphotos'">
|
||||
@destination.GooglePhotosDestinationForm()
|
||||
</template>
|
||||
|
||||
<template x-if="destinationType === 'hetzner'">
|
||||
@destination.HetznerDestinationForm()
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Options Section -->
|
||||
@@ -627,6 +682,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
@@ -668,4 +725,215 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
templ formvalidation() {
|
||||
<script>
|
||||
document.addEventListener('htmx:beforeRequest', function(evt) {
|
||||
if (evt.detail.elt.id === 'config-form') {
|
||||
const formErrors = document.getElementById('form-errors');
|
||||
const errorList = document.getElementById('error-list');
|
||||
let errors = [];
|
||||
let hasErrors = false;
|
||||
|
||||
// Clear previous errors
|
||||
errorList.innerHTML = '';
|
||||
formErrors.classList.add('hidden');
|
||||
|
||||
// Validate name
|
||||
const name = document.getElementById('name').value;
|
||||
if (!name || name.trim() === '') {
|
||||
errors.push('Configuration name is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Source path validation
|
||||
// Note: Most source and destination forms already have HTML5 validation
|
||||
// with the required attribute, but we do additional JS validation here
|
||||
// to provide a better user experience with a centralized error display
|
||||
const sourcePath = document.getElementById('source_path')?.value;
|
||||
if (!sourcePath || sourcePath.trim() === '') {
|
||||
errors.push('Source path is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Get source type
|
||||
const sourceType = document.querySelector('input[name="source_type"]').value;
|
||||
|
||||
// For remote source, validate credentials
|
||||
if (sourceType !== 'local') {
|
||||
// Host validation for remote sources
|
||||
const sourceHost = document.getElementById('source_host')?.value;
|
||||
if (!sourceHost || sourceHost.trim() === '') {
|
||||
errors.push('Source host is required for remote connections');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Port validation
|
||||
const sourcePort = document.getElementById('source_port')?.value;
|
||||
if (!sourcePort || isNaN(parseInt(sourcePort)) || parseInt(sourcePort) <= 0 || parseInt(sourcePort) > 65535) {
|
||||
errors.push('Source port must be a valid port number (1-65535)');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Username validation for SFTP/FTP/etc.
|
||||
if (['sftp', 'ftp', 'hetzner'].includes(sourceType)) {
|
||||
const sourceUser = document.getElementById('source_username')?.value;
|
||||
if (!sourceUser || sourceUser.trim() === '') {
|
||||
errors.push('Source username is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Check auth type
|
||||
const sourceAuthType = document.querySelector('input[name="source_auth_type"]:checked')?.value;
|
||||
|
||||
// Password validation if using password auth
|
||||
if (sourceAuthType === 'password') {
|
||||
const sourcePassword = document.getElementById('source_password')?.value;
|
||||
if (!sourcePassword || sourcePassword.trim() === '') {
|
||||
errors.push('Source password is required when using password authentication');
|
||||
hasErrors = true;
|
||||
}
|
||||
} else if (sourceAuthType === 'key') {
|
||||
// Key file validation if using key auth
|
||||
const sourceKeyFile = document.getElementById('source_key_file')?.value;
|
||||
if (!sourceKeyFile || sourceKeyFile.trim() === '') {
|
||||
errors.push('Source key file path is required when using key authentication');
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// S3/B2/Wasabi specific validations
|
||||
if (['s3', 'b2', 'wasabi', 'minio'].includes(sourceType)) {
|
||||
const sourceAccessKey = document.getElementById('source_access_key')?.value;
|
||||
const sourceSecretKey = document.getElementById('source_secret_key')?.value;
|
||||
|
||||
if (!sourceAccessKey || sourceAccessKey.trim() === '') {
|
||||
errors.push('Source access key is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (!sourceSecretKey || sourceSecretKey.trim() === '') {
|
||||
errors.push('Source secret key is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Bucket validation
|
||||
const sourceBucket = document.getElementById('source_bucket')?.value;
|
||||
if (!sourceBucket || sourceBucket.trim() === '') {
|
||||
errors.push('Source bucket is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate destination if it's a required field
|
||||
const requiresDestination = document.querySelector('form').__x.$data.requiresDestination;
|
||||
if (requiresDestination) {
|
||||
// Destination path validation
|
||||
const destPath = document.getElementById('destination_path')?.value;
|
||||
if (!destPath || destPath.trim() === '') {
|
||||
errors.push('Destination path is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Get destination type
|
||||
const destType = document.querySelector('input[name="destination_type"]').value;
|
||||
|
||||
// For remote destination, validate credentials
|
||||
if (destType !== 'local') {
|
||||
// Host validation for remote destinations
|
||||
const destHost = document.getElementById('destination_host')?.value;
|
||||
if (!destHost || destHost.trim() === '') {
|
||||
errors.push('Destination host is required for remote connections');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Port validation
|
||||
const destPort = document.getElementById('destination_port')?.value;
|
||||
if (!destPort || isNaN(parseInt(destPort)) || parseInt(destPort) <= 0 || parseInt(destPort) > 65535) {
|
||||
errors.push('Destination port must be a valid port number (1-65535)');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Username validation for SFTP/FTP/etc.
|
||||
if (['sftp', 'ftp', 'hetzner'].includes(destType)) {
|
||||
const destUser = document.getElementById('destination_username')?.value;
|
||||
if (!destUser || destUser.trim() === '') {
|
||||
errors.push('Destination username is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Check auth type
|
||||
const destAuthType = document.querySelector('input[name="destination_auth_type"]:checked')?.value;
|
||||
|
||||
// Password validation if using password auth
|
||||
if (destAuthType === 'password') {
|
||||
const destPassword = document.getElementById('destination_password')?.value;
|
||||
if (!destPassword || destPassword.trim() === '') {
|
||||
errors.push('Destination password is required when using password authentication');
|
||||
hasErrors = true;
|
||||
}
|
||||
} else if (destAuthType === 'key') {
|
||||
// Key file validation if using key auth
|
||||
const destKeyFile = document.getElementById('destination_key_file')?.value;
|
||||
if (!destKeyFile || destKeyFile.trim() === '') {
|
||||
errors.push('Destination key file path is required when using key authentication');
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// S3/B2/Wasabi specific validations
|
||||
if (['s3', 'b2', 'wasabi', 'minio'].includes(destType)) {
|
||||
const destAccessKey = document.getElementById('destination_access_key')?.value;
|
||||
const destSecretKey = document.getElementById('destination_secret_key')?.value;
|
||||
|
||||
if (!destAccessKey || destAccessKey.trim() === '') {
|
||||
errors.push('Destination access key is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (!destSecretKey || destSecretKey.trim() === '') {
|
||||
errors.push('Destination secret key is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Bucket validation
|
||||
const destBucket = document.getElementById('destination_bucket')?.value;
|
||||
if (!destBucket || destBucket.trim() === '') {
|
||||
errors.push('Destination bucket is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for concurrent transfers
|
||||
const maxTransfers = document.getElementById('max_concurrent_transfers')?.value;
|
||||
if (maxTransfers && (isNaN(parseInt(maxTransfers)) || parseInt(maxTransfers) < 1)) {
|
||||
errors.push('Maximum concurrent transfers must be at least 1');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// If errors exist, prevent form submission and display errors
|
||||
if (hasErrors) {
|
||||
evt.preventDefault();
|
||||
|
||||
// Display errors
|
||||
errors.forEach(error => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = error;
|
||||
errorList.appendChild(li);
|
||||
});
|
||||
|
||||
formErrors.classList.remove('hidden');
|
||||
|
||||
// Scroll to the errors
|
||||
formErrors.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -16,6 +16,8 @@ type DashboardData struct {
|
||||
FailedTransfers int
|
||||
Configs map[uint]db.TransferConfig
|
||||
RcloneVersion string
|
||||
LatestVersion string
|
||||
CurrentVersion string
|
||||
}
|
||||
|
||||
// GetRcloneVersion executes the rclone --version command and returns the version string
|
||||
@@ -41,6 +43,53 @@ func GetRcloneVersion() string {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// isNewerVersionAvailable checks if the latest version is newer than the current version
|
||||
func isNewerVersionAvailable(current, latest string) bool {
|
||||
// If either version is empty, we can't do a comparison
|
||||
if current == "" || latest == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Strip 'v' prefix if present for comparison
|
||||
if strings.HasPrefix(current, "v") {
|
||||
current = current[1:]
|
||||
}
|
||||
if strings.HasPrefix(latest, "v") {
|
||||
latest = latest[1:]
|
||||
}
|
||||
|
||||
// Split versions into components
|
||||
currentParts := strings.Split(current, ".")
|
||||
latestParts := strings.Split(latest, ".")
|
||||
|
||||
// Compare major, minor, patch versions
|
||||
for i := 0; i < len(currentParts) && i < len(latestParts); i++ {
|
||||
// Parse to integers
|
||||
currentNum, err1 := strconv.Atoi(currentParts[i])
|
||||
latestNum, err2 := strconv.Atoi(latestParts[i])
|
||||
|
||||
// If either can't be parsed, do string comparison
|
||||
if err1 != nil || err2 != nil {
|
||||
if currentParts[i] < latestParts[i] {
|
||||
return true
|
||||
} else if currentParts[i] > latestParts[i] {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Compare numbers
|
||||
if latestNum > currentNum {
|
||||
return true
|
||||
} else if latestNum < currentNum {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If all components are equal but latest has more components, it's newer
|
||||
return len(latestParts) > len(currentParts)
|
||||
}
|
||||
|
||||
templ Dashboard(ctx context.Context, data DashboardData) {
|
||||
@LayoutWithContext("Dashboard", ctx) {
|
||||
<div id="dashboard-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
@@ -253,6 +302,31 @@ templ Dashboard(ctx context.Context, data DashboardData) {
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Application Version</span>
|
||||
if data.CurrentVersion != "" && data.LatestVersion != "" && isNewerVersionAvailable(data.CurrentVersion, data.LatestVersion) {
|
||||
<div class="flex items-center">
|
||||
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-yellow-900 dark:text-yellow-300 mr-2">
|
||||
<i class="fas fa-exclamation-triangle w-3 h-3 mr-1"></i>
|
||||
{ data.CurrentVersion }
|
||||
</span>
|
||||
<a href="https://github.com/starfleetcptn/gomft/releases/tag/{ data.LatestVersion }" target="_blank"
|
||||
class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-800">
|
||||
<i class="fas fa-arrow-circle-up w-3 h-3 mr-1"></i>
|
||||
{ data.LatestVersion } Available
|
||||
</a>
|
||||
</div>
|
||||
} else {
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<i class="fas fa-check-circle w-3 h-3 mr-1"></i>
|
||||
if data.CurrentVersion != "" {
|
||||
{ data.CurrentVersion }
|
||||
} else {
|
||||
Up to date
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,12 +177,15 @@ templ NotificationDropdown(data NotificationsData) {
|
||||
|
||||
templ NotificationCount(count int64) {
|
||||
if count > 0 {
|
||||
<div class="absolute inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full -top-1 -right-1" id="notification-count">
|
||||
<div class="absolute inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 border border-white dark:border-gray-800 rounded-full -translate-y-1/2 translate-x-1/2" style="top: 0; right: 0;" id="notification-count">
|
||||
if count > 99 {
|
||||
99+
|
||||
} else {
|
||||
{ fmt.Sprintf("%d", count) }
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<!-- Empty element to ensure proper swap when count is zero -->
|
||||
<span class="hidden"></span>
|
||||
}
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package details
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/dialog" // Import dialog package
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils" // Import utils package
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// FileMetadataDetails renders the details view for a file metadata, matching original structure
|
||||
func FileMetadataDetails(ctx context.Context, data file_metadata.FileMetadataDetailsData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"file-details-container\" style=\"min-height: 100vh;\" class=\"bg-gray-50 dark:bg-gray-900\"><div class=\"pb-8 w-full\"><div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt w-6 h-6 mr-2 text-blue-500\"></i> File Details: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 26, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><a href=\"/files\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-arrow-left mr-2\"></i> Back to Files</a></div><div class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><!-- Card header --><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 37, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">File ID: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(data.File.ID), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 40, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div><!-- Card content --><div class=\"p-4 md:p-5\"><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\"><!-- File Information --><div><h6 class=\"text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt mr-2 text-gray-500 dark:text-gray-400\"></i> File Information</h6><div class=\"overflow-x-auto relative shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><tbody><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Filename</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 60, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Size</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatFileSize(data.File.FileSize))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 68, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Hash</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.File.FileHash != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.FileHash)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 77, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"text-gray-400 dark:text-gray-500 italic\">Not available</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Status</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 = []any{"text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(data.File.Status)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 89, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Original Path</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-folder mr-2 text-yellow-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.OriginalPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 100, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Destination Path</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-folder-open mr-2 text-blue-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.DestinationPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 111, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></div></td></tr></tbody></table></div></div><!-- Processing Information --><div><h6 class=\"text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-cogs mr-2 text-gray-500 dark:text-gray-400\"></i> Processing Information</h6><div class=\"overflow-x-auto relative shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><tbody><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Job</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/job/%d", data.File.JobID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.Job.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 134, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Processed Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"far fa-clock mr-2 text-gray-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ProcessedTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 145, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Creation Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-calendar-plus mr-2 text-green-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.CreationTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 156, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></div></td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Modification Time</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\"><div class=\"flex items-center\"><i class=\"fas fa-calendar-alt mr-2 text-purple-500\"></i> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ModTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 167, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.File.Status == "error" && data.File.ErrorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Error</th><td class=\"py-3 px-4 break-all bg-white dark:bg-gray-800\"><div class=\"flex items-start\"><i class=\"fas fa-exclamation-triangle mt-1 mr-2 text-red-500\"></i> <span class=\"text-red-600 dark:text-red-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 179, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span></div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Record Created</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 189, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td></tr><tr class=\"border-b dark:border-gray-700\"><th scope=\"row\" class=\"py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800\">Record Updated</th><td class=\"py-3 px-4 bg-white dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(data.File.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/details/file_metadata_details.templ`, Line: 197, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td></tr></tbody></table></div></div></div><!-- Delete dialog component call -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dialog.FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", data.File.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName),
|
||||
"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", // Use correct classes
|
||||
"Delete",
|
||||
"delete",
|
||||
data.File.ID,
|
||||
data.File.FileName,
|
||||
"details", // Indicate this is from the details view
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<!-- Action buttons --><div class=\"mt-6 flex flex-wrap justify-end gap-3\"><a href=\"/files\" class=\"py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700\"><i class=\"fas fa-list mr-2\"></i> Back to Files</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", data.File.ID)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" 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></div></div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('file-details-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('file-details-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("File Details", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,301 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package dialog
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
)
|
||||
|
||||
// FileMetadataDialog renders a confirmation dialog for file metadata actions
|
||||
func FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 11, Col: 13}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" 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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s-backdrop", id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 13, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if action == "delete" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"fas fa-trash-alt text-red-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h3 class=\"mb-5 text-lg font-normal text-gray-500 dark:text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 23, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if section == "list" {
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button type=\"button\" class=\"text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/files/%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 28, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#file-row-%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 29, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-swap=\"delete\" data-file-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 31, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-file-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 32, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-file-btn-%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 33, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 35, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button type=\"button\" class=\"text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 bg-red-600 hover:bg-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/files/%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 41, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-redirect=\"/files\" data-file-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 43, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" data-file-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 44, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-file-btn-%d", fileID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 45, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerFileDelete('%s', %d, '%s')", id, fileID, fileName)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/dialog/file_metadata_dialog.templ`, Line: 47, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" 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></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,802 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package list
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/dialog"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Helper function to generate sorting links
|
||||
func sortLink(currentSortBy, currentSortDir, targetSortBy, basePath string, filter file_metadata.FileMetadataFilter, limit int) string {
|
||||
nextSortDir := "asc"
|
||||
if currentSortBy == targetSortBy && currentSortDir == "asc" {
|
||||
nextSortDir = "desc"
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("page", "1")
|
||||
q.Set("limit", strconv.Itoa(limit))
|
||||
q.Set("sort_by", targetSortBy)
|
||||
q.Set("sort_dir", nextSortDir)
|
||||
if filter.Status != "" {
|
||||
q.Set("status", filter.Status)
|
||||
}
|
||||
if filter.FileName != "" {
|
||||
q.Set("filename", filter.FileName)
|
||||
}
|
||||
if filter.JobID != "" {
|
||||
q.Set("job_id", filter.JobID)
|
||||
}
|
||||
if filter.Hash != "" {
|
||||
q.Set("hash", filter.Hash)
|
||||
}
|
||||
if filter.StartDate != "" {
|
||||
q.Set("start_date", filter.StartDate)
|
||||
}
|
||||
if filter.EndDate != "" {
|
||||
q.Set("end_date", filter.EndDate)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s?%s", basePath, q.Encode())
|
||||
}
|
||||
|
||||
// Helper function to get sort icon class
|
||||
func sortIconClass(currentSortBy, currentSortDir, targetSortBy string) string {
|
||||
if currentSortBy == targetSortBy {
|
||||
if currentSortDir == "asc" {
|
||||
return "fas fa-sort-up ml-1"
|
||||
}
|
||||
return "fas fa-sort-down ml-1"
|
||||
}
|
||||
return "fas fa-sort text-gray-400 ml-1"
|
||||
}
|
||||
|
||||
// FileMetadataListPartial renders the list of file metadata in a table format
|
||||
// Added basePath and targetContainerID parameters
|
||||
func FileMetadataListPartial(ctx context.Context, data file_metadata.FileMetadataListData, basePath string, targetContainerID string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Container for dynamically generated dialogs --><div id=\"dialog-container\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, file := range data.Files {
|
||||
templ_7745c5c3_Err = dialog.FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", file.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", file.FileName),
|
||||
"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",
|
||||
"Delete",
|
||||
"delete",
|
||||
file.ID,
|
||||
file.FileName,
|
||||
"list",
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><!-- File List Table --><div class=\"relative overflow-x-auto shadow-md sm:rounded-lg\"><table class=\"w-full text-sm text-left text-gray-500 dark:text-gray-400\"><thead class=\"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400\"><tr><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "id", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 87, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 88, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">ID ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{sortIconClass(data.SortBy, data.SortDir, "id")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "filename", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 98, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 99, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Filename ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 = []any{sortIconClass(data.SortBy, data.SortDir, "filename")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "size", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 109, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 110, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Size ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 = []any{sortIconClass(data.SortBy, data.SortDir, "size")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "processed_time", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 120, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 121, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Processed time ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 = []any{sortIconClass(data.SortBy, data.SortDir, "processed_time")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></i></a></th><th scope=\"col\" class=\"px-6 py-3\"><a href=\"#\" hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(sortLink(data.SortBy, data.SortDir, "status", basePath, data.Filter, data.Limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 131, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 132, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" hx-swap=\"innerHTML\" class=\"flex items-center hover:text-blue-600 dark:hover:text-blue-400\">Status ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 = []any{sortIconClass(data.SortBy, data.SortDir, "status")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\"></i></a></th>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Job == nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<th scope=\"col\" class=\"px-6 py-3\">Job</th>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<th scope=\"col\" class=\"px-6 py-3\">Actions</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, file := range data.Files {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("file-row-%d", file.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 147, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600\"><td class=\"px-6 py-4 font-medium text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(file.ID), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 149, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td class=\"px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/%d", file.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var24)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" class=\"hover:underline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(file.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 153, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</a></td><td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatFileSize(file.FileSize))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 157, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(file.ProcessedTime.Format("2006-01-02 15:04:05"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 160, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td><td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 = []any{"text-xs font-medium px-2.5 py-0.5 rounded", utils.GetStatusBadgeClass(file.Status)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var28).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(file.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 164, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span></td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Job == nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<td class=\"px-6 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if file.Job.ID > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/job/%d", file.Job.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var31)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(file.Job.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 171, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<span class=\"text-gray-400 dark:text-gray-500 italic\">N/A</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<td class=\"px-6 py-4\"><div class=\"flex space-x-3\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var33 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files/%d", file.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var33)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" class=\"font-medium text-blue-600 dark:text-blue-500 hover:underline\"><i class=\"fas fa-eye\"></i></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var34 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-file-dialog-%d')", file.ID)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" data-file-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatUint(uint64(file.ID), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 186, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" data-file-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(file.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 187, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"font-medium text-red-600 dark:text-red-500 hover:underline\"><i class=\"fas fa-trash\"></i></button></div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</tbody></table></div><!-- Pagination with HTMX (Update links to include sorting and targetContainerID) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.TotalPages > 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<nav class=\"flex items-center flex-column flex-wrap md:flex-row justify-between p-4\" aria-label=\"Table navigation\"><span class=\"text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0\">Showing <span class=\"font-semibold text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa((data.Page-1)*data.Limit + 1))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 203, Col: 112}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "-")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(func() int {
|
||||
end := data.Page * data.Limit
|
||||
if int64(end) > data.TotalCount {
|
||||
return int(data.TotalCount)
|
||||
}
|
||||
return end
|
||||
}()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 209, Col: 8}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span> of <span class=\"font-semibold text-gray-900 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(data.TotalCount, 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 209, Col: 119}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></span><ul class=\"inline-flex -space-x-px rtl:space-x-reverse text-sm h-8\"><li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Page == 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span class=\"flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">Previous</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page-1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 218, Col: 232}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 219, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">Previous</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := 1; i <= data.TotalPages; i++ {
|
||||
if i == 1 || i == data.TotalPages || (i >= data.Page-2 && i <= data.Page+2) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if i == data.Page {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span aria-current=\"page\" class=\"flex items-center justify-center px-3 h-8 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 232, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<a hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var43 string
|
||||
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, i, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 235, Col: 222}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 236, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 239, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if (i == 2 && data.Page > 4) || (i == data.TotalPages-1 && data.Page < data.TotalPages-3) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<li><span class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">...</span></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Page == data.TotalPages {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<span class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg cursor-not-allowed dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\">Next</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var46 string
|
||||
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?page=%d&limit=%d&status=%s&filename=%s&job_id=%s&sort_by=%s&sort_dir=%s", basePath, data.Page+1, data.Limit, data.Filter.Status, data.Filter.FileName, data.Filter.JobID, data.SortBy, data.SortDir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 258, Col: 232}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var47 string
|
||||
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(targetContainerID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list_partial.templ`, Line: 259, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" hx-swap=\"innerHTML\" class=\"flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white\">Next</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</li></ul></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,208 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package list
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// FileMetadataList renders the list of file metadata
|
||||
func FileMetadataList(ctx context.Context, data file_metadata.FileMetadataListData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"list-container\" style=\"min-height: 100vh;\" class=\"bg-gray-50 dark:bg-gray-900\"><div class=\"pb-8 w-full\"><div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-file-alt w-6 h-6 mr-2 text-blue-500\"></i> Files</h1><div class=\"flex gap-3\"><a href=\"/files/search\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-search mr-2\"></i> Advanced Search</a></div></div><!-- Filter Form --><div class=\"p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><h5 class=\"mb-4 text-lg font-semibold text-gray-900 dark:text-white\">Filter Files</h5><form hx-get=\"/files/partial\" hx-target=\"#file-list-container\" hx-swap=\"innerHTML\" hx-indicator=\"#filter-loading\" hx-headers=\"{"X-HX-Request": "true"}\" class=\"grid grid-cols-1 md:grid-cols-3 gap-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Job == nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div><label for=\"job_id\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Job</label> <input type=\"text\" id=\"job_id\" name=\"job_id\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.JobID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 46, Col: 78}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" placeholder=\"Job ID\" 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 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\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div><label for=\"status\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Status</label> <select id=\"status\" name=\"status\" 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 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\"><option value=\"\">All Statuses</option> <option value=\"processed\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "processed" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">Processed</option> <option value=\"archived\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Archived</option> <option value=\"deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Deleted</option> <option value=\"archived_and_deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived_and_deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Archived & Deleted</option> <option value=\"error\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "error" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Error</option></select></div><div><label for=\"filename\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Filename</label> <input type=\"text\" id=\"filename\" name=\"filename\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 62, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" placeholder=\"Filename or partial match\" 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 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\"></div><div class=\"md:col-span-3 flex justify-end items-center\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-filter mr-2\"></i> Apply Filters</button><div id=\"filter-loading\" class=\"htmx-indicator ml-2 flex items-center\"><i class=\"fas fa-circle-notch fa-spin text-blue-600\"></i></div></div></form></div><div class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><!-- Card header --><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">File List</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Showing ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(int64((data.Page-1)*data.Limit+1), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " to ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(int64(min(data.Page*data.Limit, int(data.TotalCount))), 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 166}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " of ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(data.TotalCount, 10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/list/file_metadata_list.templ`, Line: 84, Col: 212}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " files</p></div><!-- Card content --><div id=\"file-list-container\" class=\"p-4 md:p-5\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = FileMetadataListPartial(ctx, data, "/files/partial", "#file-list-container").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('list-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('list-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("Files", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,75 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package search
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/list"
|
||||
)
|
||||
|
||||
// FileMetadataSearchContent renders only the search results table and pagination
|
||||
func FileMetadataSearchContent(ctx context.Context, data file_metadata.FileMetadataSearchData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Search Results --><div id=\"search-results\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data.Files) > 0 {
|
||||
templ_7745c5c3_Err = list.FileMetadataListPartial(ctx, file_metadata.FileMetadataListData{
|
||||
Files: data.Files,
|
||||
Page: data.Page,
|
||||
Limit: data.Limit,
|
||||
TotalCount: data.TotalCount,
|
||||
TotalPages: data.TotalPages,
|
||||
Filter: data.Filter, // Pass filter data for pagination links
|
||||
SortBy: data.SortBy,
|
||||
SortDir: data.SortDir,
|
||||
}, "/files/search/partial", "#search-results-container").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"p-6 text-center text-gray-500 dark:text-gray-400\"><svg class=\"mx-auto mb-4 w-12 h-12 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z\"></path></svg><p>No files found matching your search criteria.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,189 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package search
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/starfleetcptn/gomft/components" // Import the main components package
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata"
|
||||
"github.com/starfleetcptn/gomft/components/file_metadata/utils"
|
||||
)
|
||||
|
||||
// FileMetadataSearch renders the search interface for file metadata
|
||||
func FileMetadataSearch(ctx context.Context, data file_metadata.FileMetadataSearchData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages --> <div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = utils.FileMetadataJS().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div id=\"search-container\" style=\"min-height: 100vh;\" class=\"bg-gray-50 dark:bg-gray-900\"><div class=\"pb-8 w-full\"><div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-search w-6 h-6 mr-2 text-blue-500\"></i> Search Files</h1><div class=\"flex gap-3\"><a href=\"/files\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-arrow-left mr-2\"></i> Back to Files</a></div></div><!-- Advanced Search Form --><div class=\"p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full\"><h5 class=\"mb-4 text-lg font-semibold text-gray-900 dark:text-white\">Advanced File Search</h5><form hx-get=\"/files/search/partial\" hx-target=\"#search-results-container\" hx-swap=\"innerHTML\" hx-indicator=\"#search-form-loading\" hx-headers=\"{"X-HX-Request": "true"}\" hx-boost=\"false\" class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label for=\"job_id\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Job ID</label> <input type=\"text\" id=\"job_id\" name=\"job_id\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.JobID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 45, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" placeholder=\"Job ID\" 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 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\"></div><div><label for=\"status\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Status</label> <select id=\"status\" name=\"status\" 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 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\"><option value=\"\">All Statuses</option> <option value=\"processed\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "processed" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">Processed</option> <option value=\"archived\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">Archived</option> <option value=\"deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Deleted</option> <option value=\"archived_and_deleted\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "archived_and_deleted" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Archived & Deleted</option> <option value=\"error\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Filter.Status == "error" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Error</option></select></div><div><label for=\"filename\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Filename</label> <input type=\"text\" id=\"filename\" name=\"filename\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.FileName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 61, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" placeholder=\"Filename or partial match\" 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 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\"></div><div><label for=\"hash\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">File Hash</label> <input type=\"text\" id=\"hash\" name=\"hash\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.Hash)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 66, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" placeholder=\"MD5 hash\" 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 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\"></div><div><label for=\"start_date\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Processed After</label> <input type=\"date\" id=\"start_date\" name=\"start_date\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.StartDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 71, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" 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 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\"></div><div><label for=\"end_date\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Processed Before</label> <input type=\"date\" id=\"end_date\" name=\"end_date\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Filter.EndDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/file_metadata/search/file_metadata_search.templ`, Line: 76, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" 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 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\"></div><div class=\"md:col-span-2 flex justify-end\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-search mr-2\"></i> Search Files</button><div id=\"search-form-loading\" class=\"htmx-indicator ml-2 flex items-center\"><i class=\"fas fa-circle-notch fa-spin text-blue-600\"></i></div></div></form></div><!-- Results Container (Initially empty, populated by HTMX) --><div id=\"search-results-container\" class=\"bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full mt-6\"><div class=\"p-4 md:p-5 border-b border-gray-200 dark:border-gray-700\"><h5 class=\"text-xl font-bold leading-none text-gray-900 dark:text-white\">Search Results</h5><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Enter search criteria above and click \"Search Files\".</p></div><div class=\"p-4 md:p-5\"><!-- Content will be loaded here by HTMX --><div class=\"text-center text-gray-500 dark:text-gray-400 py-8\">No results yet.</div></div></div></div><script>\n\t\t\t\t// Set dark background color if in dark mode\n\t\t\t\tif (document.documentElement.classList.contains('dark')) {\n\t\t\t\t\tdocument.getElementById('search-container').style.backgroundColor = '#111827';\n\t\t\t\t}\n\n\t\t\t\t// Add event listener for theme changes\n\t\t\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\t\t\tconst themeToggle = document.getElementById('theme-toggle');\n\t\t\t\t\tif (themeToggle) {\n\t\t\t\t\t\tthemeToggle.addEventListener('click', function() {\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\t\t\t\tdocument.getElementById('search-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';\n\t\t\t\t\t\t\t}, 50);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("Search Files", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
File diff suppressed because one or more lines are too long
+1168
-14
File diff suppressed because it is too large
Load Diff
+89
-54
@@ -151,27 +151,27 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<div class="flex flex-col flex-grow pt-5 bg-white dark:bg-gray-800 overflow-y-auto border-r border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center flex-shrink-0 px-4">
|
||||
<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>
|
||||
<img src="/static/img/logo.png" alt="GoMFT" class="w-8 h-8 mr-2">
|
||||
GoMFT
|
||||
</a>
|
||||
</div>
|
||||
<nav class="flex-1 px-2 py-4 bg-white dark:bg-gray-800">
|
||||
<div class="space-y-1">
|
||||
<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>
|
||||
<i class="fas fa-chart-pie 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
|
||||
<i class="fas fa-exchange-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Transfer Configurations
|
||||
</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
|
||||
<i class="fas fa-calendar-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Scheduled 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
|
||||
Transfer 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>
|
||||
@@ -183,16 +183,20 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
</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
|
||||
User Management
|
||||
</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
|
||||
Role Management
|
||||
</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/logs" 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-stream w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Log Viewer
|
||||
</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
|
||||
@@ -246,8 +250,8 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<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
|
||||
<!-- GoMFT Logo -->
|
||||
<img src="/static/img/logo.png" alt="GoMFT" class="w-8 h-8 mr-2">
|
||||
</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>
|
||||
@@ -258,20 +262,20 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<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>
|
||||
<i class="fas fa-chart-pie 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
|
||||
<i class="fas fa-exchange-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Transfer Configurations
|
||||
</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
|
||||
<i class="fas fa-calendar-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
||||
Scheduled 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
|
||||
Transfer 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>
|
||||
@@ -283,29 +287,33 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
</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
|
||||
User Management
|
||||
</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
|
||||
Role Management
|
||||
</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/logs" 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-stream w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
||||
Log Viewer
|
||||
</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">
|
||||
<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-mobile" data-collapse-toggle="dropdown-settings-mobile">
|
||||
<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">
|
||||
<ul id="dropdown-settings-mobile" 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>
|
||||
@@ -332,7 +340,7 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<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">
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
|
||||
// <h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
|
||||
</div>
|
||||
<!-- Theme toggle and user menu -->
|
||||
<div class="flex items-center space-x-4">
|
||||
@@ -360,40 +368,41 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
>
|
||||
<span class="sr-only">View notifications</span>
|
||||
<i class="fas fa-bell"></i>
|
||||
<!-- Notification badge will be loaded dynamically -->
|
||||
<div hx-get="/notifications/count" hx-trigger="load, notification-updated from:body" id="notification-count-container"></div>
|
||||
<!-- Notification count will be placed here via HTMX -->
|
||||
</button>
|
||||
<!-- Notification dropdown -->
|
||||
<div
|
||||
class="hidden overflow-hidden z-50 my-4 max-w-sm md:max-w-md w-full text-base list-none bg-white rounded divide-y divide-gray-100 shadow-lg dark:divide-gray-600 dark:bg-gray-700 rounded-xl"
|
||||
id="notification-dropdown"
|
||||
style="min-width: 320px; width: 100%;"
|
||||
data-dropdown-placement="bottom-end"
|
||||
>
|
||||
<div id="notification-dropdown-content">
|
||||
<!-- Content will be loaded dynamically via HTMX -->
|
||||
<div class="block py-2 px-4 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-600 dark:text-gray-300">
|
||||
Notifications
|
||||
</div>
|
||||
<div class="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="animate-pulse flex flex-col items-center">
|
||||
<div class="rounded-full bg-gray-200 dark:bg-gray-700 h-12 w-12 mb-2"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/notifications" class="block py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:underline">
|
||||
<div class="inline-flex items-center">
|
||||
<svg aria-hidden="true" class="mr-2 w-4 h-4 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
View all
|
||||
</div>
|
||||
</a>
|
||||
<!-- Place the notification count container outside but still in the relative container -->
|
||||
<div id="notification-count-container" class="absolute top-0 right-0" hx-get="/notifications/count" hx-trigger="load, notification-updated from:body"></div>
|
||||
</div>
|
||||
<!-- Notification dropdown -->
|
||||
<div
|
||||
class="hidden overflow-hidden z-50 my-4 max-w-sm md:max-w-md w-full text-base list-none bg-white rounded divide-y divide-gray-100 shadow-lg dark:divide-gray-600 dark:bg-gray-700 rounded-xl"
|
||||
id="notification-dropdown"
|
||||
style="min-width: 320px; width: 100%;"
|
||||
data-dropdown-placement="bottom-end"
|
||||
>
|
||||
<div id="notification-dropdown-content">
|
||||
<!-- Content will be loaded dynamically via HTMX -->
|
||||
<div class="block py-2 px-4 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-600 dark:text-gray-300">
|
||||
Notifications
|
||||
</div>
|
||||
<div class="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="animate-pulse flex flex-col items-center">
|
||||
<div class="rounded-full bg-gray-200 dark:bg-gray-700 h-12 w-12 mb-2"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/notifications" class="block py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:underline">
|
||||
<div class="inline-flex items-center">
|
||||
<svg aria-hidden="true" class="mr-2 w-4 h-4 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
View all
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User menu dropdown -->
|
||||
@@ -466,6 +475,9 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<i class="fab fa-github"></i>
|
||||
</a>
|
||||
<a href="https://discord.gg/f9dwtM3j" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank">
|
||||
<i class="fab fa-discord"></i>
|
||||
</a>
|
||||
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<i class="fas fa-tag"></i> { AppVersion }
|
||||
</a>
|
||||
@@ -489,6 +501,9 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<i class="fab fa-github"></i>
|
||||
</a>
|
||||
<a href="https://discord.gg/f9dwtM3j" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank">
|
||||
<i class="fab fa-discord"></i>
|
||||
</a>
|
||||
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<i class="fas fa-tag"></i> { AppVersion }
|
||||
</a>
|
||||
@@ -506,6 +521,26 @@ templ LayoutWithContext(title string, ctx context.Context) {
|
||||
<!-- Application scripts -->
|
||||
<script defer src="/static/dist/app.js"></script>
|
||||
<script defer src="/static/dist/init.js"></script>
|
||||
<!-- Dropdown fix for mobile -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Initialize the mobile dropdown separately
|
||||
const mobileDropdownButton = document.querySelector('[data-collapse-toggle="dropdown-settings-mobile"]');
|
||||
const mobileDropdown = document.getElementById("dropdown-settings-mobile");
|
||||
|
||||
if (mobileDropdownButton && mobileDropdown) {
|
||||
// Show dropdown if on admin pages
|
||||
if (window.location.pathname.startsWith("/admin")) {
|
||||
// Keep it hidden by default, will be toggled by button
|
||||
mobileDropdown.classList.add("hidden");
|
||||
}
|
||||
|
||||
mobileDropdownButton.addEventListener("click", function() {
|
||||
mobileDropdown.classList.toggle("hidden");
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
+10
-8
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
templ Login(ctx context.Context, errorMessage string) {
|
||||
templ Login(ctx context.Context, errorMessage string, hasExternalProviders bool) {
|
||||
@LayoutWithContext("Login", ctx) {
|
||||
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
@@ -116,15 +116,17 @@ templ Login(ctx context.Context, errorMessage string) {
|
||||
Contact an administrator to create an account
|
||||
</p>
|
||||
|
||||
<!-- External Authentication Providers -->
|
||||
<div id="external-auth-providers" class="mt-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mb-3">Or sign in with:</p>
|
||||
<div id="provider-buttons" class="flex flex-col gap-2" hx-get="/auth/providers" hx-trigger="load" hx-target="#provider-buttons">
|
||||
<div class="animate-pulse flex justify-center">
|
||||
<div class="h-10 bg-gray-200 rounded w-full max-w-[200px] dark:bg-gray-700"></div>
|
||||
if hasExternalProviders {
|
||||
<!-- External Authentication Providers -->
|
||||
<div id="external-auth-providers" class="mt-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mb-3">Or sign in with:</p>
|
||||
<div id="provider-buttons" class="flex flex-col gap-2" hx-get="/auth/providers" hx-trigger="load" hx-target="#provider-buttons">
|
||||
<div class="animate-pulse flex justify-center">
|
||||
<div class="h-10 bg-gray-200 rounded w-full max-w-[200px] dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,985 +0,0 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type NotificationFormData struct {
|
||||
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
|
||||
}
|
||||
IsNew bool
|
||||
SuccessMessage string
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// Helper function to check if a string is in a slice
|
||||
func contains(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Helper function to convert bool to string for HTML attributes
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getNotificationFormTitle(isNew bool) string {
|
||||
if isNew {
|
||||
return "Add Notification Service"
|
||||
}
|
||||
return "Edit Notification Service"
|
||||
}
|
||||
|
||||
templ NotificationForm(ctx context.Context, data NotificationFormData) {
|
||||
@LayoutWithContext(getNotificationFormTitle(data.IsNew), ctx) {
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Notification system
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Toggle notification fields based on selection
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const typeSelector = document.getElementById('notification_type');
|
||||
const allFields = document.querySelectorAll('.notification-fields');
|
||||
const commonFields = document.querySelectorAll('.common-fields');
|
||||
|
||||
typeSelector.addEventListener('change', function() {
|
||||
// Hide all fields first
|
||||
allFields.forEach(field => field.classList.add('hidden'));
|
||||
|
||||
// Show/hide common fields based on selection
|
||||
const selectedType = this.value;
|
||||
if (selectedType) {
|
||||
// Show common fields (name, description)
|
||||
commonFields.forEach(field => field.classList.remove('hidden'));
|
||||
|
||||
// Show the selected type's specific fields
|
||||
const fieldsToShow = document.getElementById(`${selectedType}_fields`);
|
||||
if (fieldsToShow) {
|
||||
fieldsToShow.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
// Hide common fields if no type selected
|
||||
commonFields.forEach(field => field.classList.add('hidden'));
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize form if editing
|
||||
if (document.getElementById('notification_type').value) {
|
||||
// Trigger the change event to show the appropriate fields
|
||||
document.getElementById('notification_type').dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="notification-form-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="notifications-page bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full max-w-4xl mx-auto">
|
||||
<!-- Success Message (hidden, used for HTMX responses) -->
|
||||
if data.SuccessMessage != "" {
|
||||
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||
}
|
||||
<!-- Error Message (hidden, used for HTMX responses) -->
|
||||
if data.ErrorMessage != "" {
|
||||
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||
}
|
||||
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||
{ getNotificationFormTitle(data.IsNew) }
|
||||
</h1>
|
||||
<a href="/admin/settings/notifications" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
|
||||
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
|
||||
Back to Notification Services
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Add Notification Service Form -->
|
||||
<div class="mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<form id="notification-form"
|
||||
if data.IsNew {
|
||||
hx-post="/admin/settings/notifications"
|
||||
} else {
|
||||
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
||||
}
|
||||
hx-target="body">
|
||||
<div class="mb-6">
|
||||
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
||||
<select id="notification_type" name="type" 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 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">
|
||||
<option value="">Select a type</option>
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
||||
<option value="webhook" selected="selected">Webhook</option>
|
||||
} else {
|
||||
<option value="webhook">Webhook</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
||||
<option value="pushbullet" selected="selected">Pushbullet</option>
|
||||
} else {
|
||||
<option value="pushbullet">Pushbullet</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
||||
<option value="ntfy" selected="selected">Ntfy</option>
|
||||
} else {
|
||||
<option value="ntfy">Ntfy</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
||||
<option value="gotify" selected="selected">Gotify</option>
|
||||
} else {
|
||||
<option value="gotify">Gotify</option>
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
||||
<option value="pushover" selected="selected">Pushover</option>
|
||||
} else {
|
||||
<option value="pushover">Pushover</option>
|
||||
}
|
||||
<option value="email" disabled>Email (Coming Soon)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6 hidden common-fields">
|
||||
<label for="notification_name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
||||
if data.NotificationService.Name != "" {
|
||||
<input type="text" id="notification_name" name="name" 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 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 Notification Service" required value={ data.NotificationService.Name }/>
|
||||
} else {
|
||||
<input type="text" id="notification_name" name="name" 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 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 Notification Service" required value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6 hidden common-fields">
|
||||
<label for="notification_description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
|
||||
if data.NotificationService.Description != "" {
|
||||
<textarea id="notification_description" name="description" rows="3" 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 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="Description for this notification service">{ data.NotificationService.Description }</textarea>
|
||||
} else {
|
||||
<textarea id="notification_description" name="description" rows="3" 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 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="Description for this notification service"></textarea>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Dynamic fields based on notification type -->
|
||||
<div id="email_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="smtp_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Host</label>
|
||||
<input type="text" id="smtp_host" name="smtp_host" 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 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="smtp.example.com"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="smtp_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Port</label>
|
||||
<input type="number" id="smtp_port" name="smtp_port" 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 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="587"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="smtp_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Username</label>
|
||||
<input type="text" id="smtp_username" name="smtp_username" 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 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="user@example.com"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="smtp_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SMTP Password</label>
|
||||
<input type="password" id="smtp_password" name="smtp_password" 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 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"/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="from_email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">From Email</label>
|
||||
<input type="email" id="from_email" name="from_email" 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 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="notifications@example.com"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="webhook_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="webhook_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Webhook URL</label>
|
||||
if data.NotificationService.WebhookURL != "" {
|
||||
<input type="url" id="webhook_url" name="webhook_url" 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 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://api.example.com/webhook" value={ data.NotificationService.WebhookURL }/>
|
||||
} else {
|
||||
<input type="url" id="webhook_url" name="webhook_url" 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 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://api.example.com/webhook" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="method" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">HTTP Method</label>
|
||||
<select id="method" name="method" 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 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">
|
||||
if data.NotificationService.Method != "" {
|
||||
if data.NotificationService.Method == "POST" {
|
||||
<option value="POST" selected="selected">POST</option>
|
||||
} else {
|
||||
<option value="POST">POST</option>
|
||||
}
|
||||
if data.NotificationService.Method == "PUT" {
|
||||
<option value="PUT" selected="selected">PUT</option>
|
||||
} else {
|
||||
<option value="PUT">PUT</option>
|
||||
}
|
||||
} else {
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="headers" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Headers (JSON)</label>
|
||||
if data.NotificationService.Headers != "" {
|
||||
<textarea id="headers" name="headers" rows="3" 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 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='{"Content-Type": "application/json", "Authorization": "Bearer token"}'>{ data.NotificationService.Headers }</textarea>
|
||||
} else {
|
||||
<textarea id="headers" name="headers" rows="3" 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 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='{"Content-Type": "application/json", "Authorization": "Bearer token"}'></textarea>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="payload_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Payload Template (JSON)</label>
|
||||
<textarea
|
||||
id="payload_template"
|
||||
name="payload_template"
|
||||
rows="5"
|
||||
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 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='{
|
||||
"event": "{{job.event}}",
|
||||
"job": {
|
||||
"id": "{{job.id}}",
|
||||
"name": "{{job.name}}",
|
||||
"status": "{{job.status}}",
|
||||
"message": "{{job.message}}",
|
||||
"started_at": "{{job.started_at}}",
|
||||
"completed_at": "{{job.completed_at}}",
|
||||
"duration_seconds": {{job.duration_seconds}},
|
||||
"config_id": "{{job.config_id}}",
|
||||
"config_name": "{{job.config_name}}",
|
||||
"transfer_bytes": {{job.transfer_bytes}},
|
||||
"file_count": {{job.file_count}}
|
||||
},
|
||||
"instance": {
|
||||
"id": "{{instance.id}}",
|
||||
"name": "{{instance.name}}",
|
||||
"version": "{{instance.version}}",
|
||||
"environment": "{{instance.environment}}"
|
||||
},
|
||||
"timestamp": "{{timestamp}}",
|
||||
"notification_id": "{{notification.id}}"
|
||||
}'
|
||||
>
|
||||
if data.NotificationService.PayloadTemplate != "" {
|
||||
data.NotificationService.PayloadTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||
}
|
||||
<label for="trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||
}
|
||||
<label for="trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||
}
|
||||
<label for="trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (for signature verification)</label>
|
||||
if data.NotificationService.SecretKey != "" {
|
||||
<input type="text" id="secret_key" name="secret_key" 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 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="Optional signature verification key" value={ data.NotificationService.SecretKey }/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">If provided, all webhooks will include an X-GoMFT-Signature header</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="retry_policy" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Retry Policy</label>
|
||||
<select id="retry_policy" name="retry_policy" 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 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">
|
||||
if data.NotificationService.RetryPolicy != "" {
|
||||
if data.NotificationService.RetryPolicy == "none" {
|
||||
<option value="none" selected="selected">No retries</option>
|
||||
} else {
|
||||
<option value="none">No retries</option>
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "simple" {
|
||||
<option value="simple" selected="selected">Simple (3 retries)</option>
|
||||
} else {
|
||||
<option value="simple">Simple (3 retries)</option>
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "exponential" {
|
||||
<option value="exponential" selected="selected">Exponential backoff</option>
|
||||
} else {
|
||||
<option value="exponential">Exponential backoff</option>
|
||||
}
|
||||
} else {
|
||||
<option value="none">No retries</option>
|
||||
<option value="simple">Simple (3 retries)</option>
|
||||
<option value="exponential">Exponential backoff</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Test notification button -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-webhook-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pushbullet Fields -->
|
||||
<div id="pushbullet_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_api_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Key</label>
|
||||
if data.NotificationService.PushbulletAPIKey != "" {
|
||||
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" 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 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="o.XyzAbCdEfGhIjKlMnOpQrSt" value={ data.NotificationService.PushbulletAPIKey }/>
|
||||
} else {
|
||||
<input type="text" id="pushbullet_api_key" name="pushbullet_api_key" 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 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="o.XyzAbCdEfGhIjKlMnOpQrSt" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get your API key from <a href="https://www.pushbullet.com/#settings/account" target="_blank" class="text-blue-500 hover:underline">Pushbullet Account Settings</a></p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_device_iden" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Identifier (Optional)</label>
|
||||
if data.NotificationService.PushbulletDeviceID != "" {
|
||||
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" 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 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="Leave empty to send to all devices" value={ data.NotificationService.PushbulletDeviceID }/>
|
||||
} else {
|
||||
<input type="text" id="pushbullet_device_iden" name="pushbullet_device_iden" 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 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="Leave empty to send to all devices" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||
if data.NotificationService.PushbulletTitleTemplate != "" {
|
||||
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" 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 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" value={ data.NotificationService.PushbulletTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="pushbullet_title_template" name="pushbullet_title_template" 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 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="{{job.name}} {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushbullet_body_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="pushbullet_body_template"
|
||||
name="pushbullet_body_template"
|
||||
rows="4"
|
||||
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 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="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.PushbulletBodyTemplate != "" {
|
||||
data.NotificationService.PushbulletBodyTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="pb_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||
} else {
|
||||
<input id="pb_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="pb_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="pb_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||
} else {
|
||||
<input id="pb_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="pb_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="pb_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||
} else {
|
||||
<input id="pb_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="pb_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Test notification button for Pushbullet -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-pushbullet-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Pushbullet configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ntfy Fields -->
|
||||
<div id="ntfy_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_server" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Ntfy Server</label>
|
||||
if data.NotificationService.NtfyServer != "" {
|
||||
<input type="url" id="ntfy_server" name="ntfy_server" 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 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://ntfy.sh" value={ data.NotificationService.NtfyServer }/>
|
||||
} else {
|
||||
<input type="url" id="ntfy_server" name="ntfy_server" 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 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://ntfy.sh" value="https://ntfy.sh"/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">The Ntfy server URL (default: ntfy.sh)</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_topic" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Topic</label>
|
||||
if data.NotificationService.NtfyTopic != "" {
|
||||
<input type="text" id="ntfy_topic" name="ntfy_topic" 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 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-unique-topic" value={ data.NotificationService.NtfyTopic }/>
|
||||
} else {
|
||||
<input type="text" id="ntfy_topic" name="ntfy_topic" 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 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-unique-topic" value="gomft"/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Choose a unique, unguessable topic name</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||
<select id="ntfy_priority" name="ntfy_priority" 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 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">
|
||||
<option value="1">Low (1)</option>
|
||||
if data.NotificationService.NtfyPriority == "3" {
|
||||
<option value="3" selected="selected">Default (3)</option>
|
||||
} else {
|
||||
<option value="3">Default (3)</option>
|
||||
}
|
||||
<option value="4">High (4)</option>
|
||||
<option value="5">Urgent (5)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username (Optional)</label>
|
||||
if data.NotificationService.NtfyUsername != "" {
|
||||
<input type="text" id="ntfy_username" name="ntfy_username" 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 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 for protected topics" value={ data.NotificationService.NtfyUsername }/>
|
||||
} else {
|
||||
<input type="text" id="ntfy_username" name="ntfy_username" 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 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 for protected topics" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password (Optional)</label>
|
||||
if data.NotificationService.NtfyPassword != "" {
|
||||
<input type="password" id="ntfy_password" name="ntfy_password" 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 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="Password for protected topics" value={ data.NotificationService.NtfyPassword }/>
|
||||
} else {
|
||||
<input type="password" id="ntfy_password" name="ntfy_password" 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 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="Password for protected topics" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Title Template</label>
|
||||
if data.NotificationService.NtfyTitleTemplate != "" {
|
||||
<input type="text" id="ntfy_title_template" name="ntfy_title_template" 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 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" value={ data.NotificationService.NtfyTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="ntfy_title_template" name="ntfy_title_template" 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 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="{{job.name}} {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="ntfy_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="ntfy_message_template"
|
||||
name="ntfy_message_template"
|
||||
rows="4"
|
||||
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 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="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.NtfyMessageTemplate != "" {
|
||||
data.NotificationService.NtfyMessageTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="ntfy_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||
} else {
|
||||
<input id="ntfy_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="ntfy_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="ntfy_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||
} else {
|
||||
<input id="ntfy_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="ntfy_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="ntfy_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||
} else {
|
||||
<input id="ntfy_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="ntfy_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Test notification button for Ntfy -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-ntfy-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Ntfy configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gotify Fields -->
|
||||
<div id="gotify_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="gotify_url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Gotify Server URL</label>
|
||||
if data.NotificationService.GotifyURL != "" {
|
||||
<input type="url" id="gotify_url" name="gotify_url" 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 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://gotify.example.com" value={ data.NotificationService.GotifyURL }/>
|
||||
} else {
|
||||
<input type="url" id="gotify_url" name="gotify_url" 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 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://gotify.example.com" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">URL of your Gotify server</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Token</label>
|
||||
if data.NotificationService.GotifyToken != "" {
|
||||
<input type="text" id="gotify_token" name="gotify_token" 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 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="A-M-XiEQj.zX5d" value={ data.NotificationService.GotifyToken }/>
|
||||
} else {
|
||||
<input type="text" id="gotify_token" name="gotify_token" 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 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="A-M-XiEQj.zX5d" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Find this in your Gotify application settings</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||
<select id="gotify_priority" name="gotify_priority" 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 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">
|
||||
<option value="0">Low (0)</option>
|
||||
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
||||
<option value="5" selected="selected">Normal (5)</option>
|
||||
} else {
|
||||
<option value="5">Normal (5)</option>
|
||||
}
|
||||
<option value="8">High (8)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||
if data.NotificationService.GotifyTitleTemplate != "" {
|
||||
<input type="text" id="gotify_title_template" name="gotify_title_template" 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 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" value={ data.NotificationService.GotifyTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="gotify_title_template" name="gotify_title_template" 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 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="{{job.name}} {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="gotify_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="gotify_message_template"
|
||||
name="gotify_message_template"
|
||||
rows="4"
|
||||
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 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="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.GotifyMessageTemplate != "" {
|
||||
data.NotificationService.GotifyMessageTemplate
|
||||
}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="gotify_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||
} else {
|
||||
<input id="gotify_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="gotify_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="gotify_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||
} else {
|
||||
<input id="gotify_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="gotify_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="gotify_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||
} else {
|
||||
<input id="gotify_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="gotify_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Test notification button for Gotify -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-gotify-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Gotify configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pushover Fields -->
|
||||
<div id="pushover_fields" class="hidden notification-fields">
|
||||
<div class="mb-6">
|
||||
<label for="pushover_app_token" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">API Token/Key</label>
|
||||
if data.NotificationService.PushoverAPIToken != "" {
|
||||
<input type="text" id="pushover_app_token" name="pushover_app_token" 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 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="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value={ data.NotificationService.PushoverAPIToken }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_app_token" name="pushover_app_token" 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 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="azGDORePK8gMaC0QOYAMyEEuzJnyUi" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your application's API token/key from <a href="https://pushover.net/apps" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_user_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">User Key</label>
|
||||
if data.NotificationService.PushoverUserKey != "" {
|
||||
<input type="text" id="pushover_user_key" name="pushover_user_key" 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 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="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value={ data.NotificationService.PushoverUserKey }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_user_key" name="pushover_user_key" 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 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="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" value=""/>
|
||||
}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Your user key from <a href="https://pushover.net/" target="_blank" class="text-blue-500 hover:underline">Pushover Dashboard</a></p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_device" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Device Name (Optional)</label>
|
||||
if data.NotificationService.PushoverDevice != "" {
|
||||
<input type="text" id="pushover_device" name="pushover_device" 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 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="Leave empty to send to all devices" value={ data.NotificationService.PushoverDevice }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_device" name="pushover_device" 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 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="Leave empty to send to all devices" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Priority</label>
|
||||
<select id="pushover_priority" name="pushover_priority" 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 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">
|
||||
<option value="-2">Lowest (-2)</option>
|
||||
<option value="-1">Low (-1)</option>
|
||||
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
||||
<option value="0" selected="selected">Normal (0)</option>
|
||||
} else {
|
||||
<option value="0">Normal (0)</option>
|
||||
}
|
||||
<option value="1">High (1)</option>
|
||||
<option value="2">Emergency (2)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_sound" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Sound</label>
|
||||
<select id="pushover_sound" name="pushover_sound" 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 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">
|
||||
<option value="pushover">Pushover (default)</option>
|
||||
<option value="bike">Bike</option>
|
||||
<option value="bugle">Bugle</option>
|
||||
<option value="cashregister">Cash Register</option>
|
||||
<option value="classical">Classical</option>
|
||||
<option value="cosmic">Cosmic</option>
|
||||
<option value="falling">Falling</option>
|
||||
<option value="gamelan">Gamelan</option>
|
||||
<option value="incoming">Incoming</option>
|
||||
<option value="intermission">Intermission</option>
|
||||
<option value="magic">Magic</option>
|
||||
<option value="mechanical">Mechanical</option>
|
||||
<option value="pianobar">Piano Bar</option>
|
||||
<option value="siren">Siren</option>
|
||||
<option value="spacealarm">Space Alarm</option>
|
||||
<option value="tugboat">Tug Boat</option>
|
||||
<option value="alien">Alien Alarm (long)</option>
|
||||
<option value="climb">Climb (long)</option>
|
||||
<option value="persistent">Persistent (long)</option>
|
||||
<option value="echo">Echo (long)</option>
|
||||
<option value="updown">Up Down (long)</option>
|
||||
<option value="vibrate">Vibrate Only</option>
|
||||
<option value="none">None (silent)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_title_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Title Template</label>
|
||||
if data.NotificationService.PushoverTitleTemplate != "" {
|
||||
<input type="text" id="pushover_title_template" name="pushover_title_template" 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 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" value={ data.NotificationService.PushoverTitleTemplate }/>
|
||||
} else {
|
||||
<input type="text" id="pushover_title_template" name="pushover_title_template" 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 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="'{{job.name}}' {{job.status}}" value=""/>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="pushover_message_template" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message Body Template</label>
|
||||
<textarea
|
||||
id="pushover_message_template"
|
||||
name="pushover_message_template"
|
||||
rows="4"
|
||||
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 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="Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes)."
|
||||
>
|
||||
if data.NotificationService.PushoverMessageTemplate != "" {
|
||||
data.NotificationService.PushoverMessageTemplate
|
||||
} </textarea>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Event Triggers</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="pushover_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_start")) }/>
|
||||
} else {
|
||||
<input id="pushover_trigger_job_start" name="trigger_job_start" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="pushover_trigger_job_start" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Start</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="pushover_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_complete")) }/>
|
||||
} else {
|
||||
<input id="pushover_trigger_job_complete" name="trigger_job_complete" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="pushover_trigger_job_complete" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Complete</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
if data.NotificationService.EventTriggers != nil {
|
||||
<input id="pushover_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(contains(data.NotificationService.EventTriggers, "job_error")) }/>
|
||||
} else {
|
||||
<input id="pushover_trigger_job_error" name="trigger_job_error" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"/>
|
||||
}
|
||||
<label for="pushover_trigger_job_error" class="ml-2 text-sm font-medium text-gray-900 dark:text-white">Job Error</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Test notification button for Pushover -->
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">Test Configuration</h4>
|
||||
<button
|
||||
type="button"
|
||||
id="test-pushover-btn"
|
||||
hx-post="/admin/settings/notifications/test"
|
||||
hx-trigger="click"
|
||||
hx-target="#test-notification-result"
|
||||
hx-swap="outerHTML"
|
||||
class="px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-paper-plane mr-1"></i>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Send a test notification to verify your Pushover configuration works correctly before saving.
|
||||
</p>
|
||||
<div id="test-notification-result" class="mt-3 hidden">
|
||||
<!-- Result will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start mb-6 hidden common-fields">
|
||||
<div class="flex items-center h-5">
|
||||
if data.NotificationService.IsEnabled != false {
|
||||
<input id="is_enabled" name="is_enabled" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked={ boolToString(data.NotificationService.IsEnabled) }/>
|
||||
} else {
|
||||
<input id="is_enabled" name="is_enabled" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" checked="true"/>
|
||||
}
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="is_enabled" class="font-medium text-gray-900 dark:text-white">Enable this notification service</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden common-fields">
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
if data.IsNew {
|
||||
Add Service
|
||||
} else {
|
||||
Save Changes
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Help Notice -->
|
||||
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||
Configure your notification service to receive alerts for job events. Different notification types have different configuration options.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('notification-form-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('notification-form-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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 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">
|
||||
if action == "delete" {
|
||||
<i class="fas fa-trash-alt text-red-400 text-3xl mb-4"></i>
|
||||
} else {
|
||||
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
|
||||
}
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{ message }</h3>
|
||||
<button
|
||||
type="button"
|
||||
class={ confirmClass }
|
||||
hx-delete={ fmt.Sprintf("/admin/settings/notifications/%d", serviceID) }
|
||||
hx-target="body"
|
||||
data-service-name={ serviceName }
|
||||
data-service-id={ fmt.Sprint(serviceID) }
|
||||
id={ fmt.Sprintf("delete-btn-%d", serviceID) }
|
||||
onclick={ triggerServiceDelete(id, serviceID, serviceName) }>
|
||||
{ confirmText }
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
script triggerServiceDelete(dialogId string, serviceID uint, serviceName string) {
|
||||
// Hide the dialog
|
||||
document.getElementById(dialogId).classList.add("hidden");
|
||||
document.getElementById(dialogId).classList.remove("flex");
|
||||
|
||||
// Add debugging info
|
||||
console.log(`Notification service deletion triggered for: ${serviceName} (ID: ${serviceID})`);
|
||||
|
||||
// Store data in a way that's accessible to event handlers
|
||||
window.lastDeletedService = {
|
||||
id: serviceID,
|
||||
name: serviceName
|
||||
};
|
||||
|
||||
// Add custom marker to track this deletion
|
||||
window.currentlyDeletingService = true;
|
||||
}
|
||||
|
||||
templ Notifications(ctx context.Context, data SettingsNotificationsData) {
|
||||
@LayoutWithContext("Notification Services", ctx) {
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="toast-container" class="fixed top-5 right-5 z-50 flex flex-col gap-2"></div>
|
||||
|
||||
<script>
|
||||
// Notification system
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
let iconClass, bgColorClass, textColorClass;
|
||||
|
||||
if (type === 'success') {
|
||||
iconClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';
|
||||
bgColorClass = 'text-green-500 dark:text-green-200';
|
||||
textColorClass = 'text-green-500 dark:text-green-200';
|
||||
} else if (type === 'error') {
|
||||
iconClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';
|
||||
bgColorClass = 'text-red-500 dark:text-red-200';
|
||||
textColorClass = 'text-red-500 dark:text-red-200';
|
||||
} else {
|
||||
iconClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';
|
||||
bgColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Track all HTMX events for debugging
|
||||
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;
|
||||
|
||||
console.log(`Request path: ${path}, method: ${method}`);
|
||||
|
||||
// Pattern match for notification service deletions (e.g., /admin/settings/notifications/123)
|
||||
if (path && method === 'DELETE' && path.match(/^\/admin\/settings\/notifications\/\d+$/)) {
|
||||
console.log("Detected notification service deletion request via URL pattern");
|
||||
|
||||
// This is definitely a delete request - store this information
|
||||
window.isServiceDeleteRequest = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', function(event) {
|
||||
// Check for notification service deletion multiple ways
|
||||
const isDeleteRequest =
|
||||
// Check global flag from the triggerServiceDelete function
|
||||
window.currentlyDeletingService ||
|
||||
// Check flag from beforeRequest handler
|
||||
window.isServiceDeleteRequest ||
|
||||
// Check URL pattern directly from this event
|
||||
(event.detail.pathInfo &&
|
||||
event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
console.log(`Is delete request: ${isDeleteRequest}`);
|
||||
|
||||
// If this is a successful delete request, show notification
|
||||
if (isDeleteRequest && event.detail.successful) {
|
||||
console.log("Delete request was successful");
|
||||
|
||||
let serviceName = "Unknown";
|
||||
|
||||
// Try multiple sources for service name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||
}
|
||||
|
||||
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||
// Fallback to our stored service info
|
||||
serviceName = window.lastDeletedService.name;
|
||||
}
|
||||
|
||||
console.log(`Showing success notification for deleted service: ${serviceName}`);
|
||||
showToast(`Notification service "${serviceName}" deleted successfully`, 'success');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingService = false;
|
||||
window.isServiceDeleteRequest = false;
|
||||
window.lastDeletedService = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:responseError', function(event) {
|
||||
console.log("HTMX response error:", event.detail);
|
||||
|
||||
// Similar logic as success but for errors
|
||||
const isDeleteRequest =
|
||||
window.currentlyDeletingService ||
|
||||
window.isServiceDeleteRequest ||
|
||||
(event.detail.pathInfo &&
|
||||
event.detail.pathInfo.requestPath &&
|
||||
event.detail.pathInfo.requestPath.match(/^\/admin\/settings\/notifications\/\d+$/) &&
|
||||
event.detail.verb === 'DELETE');
|
||||
|
||||
let errorMsg = 'An error occurred';
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = event.detail.xhr.responseText;
|
||||
}
|
||||
|
||||
if (isDeleteRequest) {
|
||||
console.log("Delete request failed");
|
||||
|
||||
let serviceName = "Unknown";
|
||||
|
||||
// Try multiple sources for service name
|
||||
if (event.detail.elt && event.detail.elt.getAttribute) {
|
||||
serviceName = event.detail.elt.getAttribute('data-service-name') || serviceName;
|
||||
}
|
||||
|
||||
if (serviceName === "Unknown" && window.lastDeletedService) {
|
||||
// Fallback to our stored service info
|
||||
serviceName = window.lastDeletedService.name;
|
||||
}
|
||||
|
||||
let errorMsg = `Failed to delete notification service "${serviceName}"`;
|
||||
|
||||
if (event.detail.xhr && event.detail.xhr.responseText) {
|
||||
errorMsg = `Error: ${event.detail.xhr.responseText}`;
|
||||
}
|
||||
|
||||
console.log(`Showing error notification: ${errorMsg}`);
|
||||
showToast(errorMsg, 'error');
|
||||
|
||||
// Clear flags
|
||||
window.currentlyDeletingService = false;
|
||||
window.isServiceDeleteRequest = false;
|
||||
window.lastDeletedService = null;
|
||||
} else {
|
||||
showToast(errorMsg, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle modal hide buttons
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hideButtons = document.querySelectorAll('[data-modal-hide]');
|
||||
hideButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modalId = this.getAttribute('data-modal-hide');
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
});
|
||||
});
|
||||
|
||||
// Show any success or error messages as toasts
|
||||
if (document.querySelector('.success-message')) {
|
||||
const successMsg = document.querySelector('.success-message').textContent.trim();
|
||||
if (successMsg) {
|
||||
showToast(successMsg, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.querySelector('.error-message')) {
|
||||
const errorMsg = document.querySelector('.error-message').textContent.trim();
|
||||
if (errorMsg) {
|
||||
showToast(errorMsg, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="notifications-container" style="min-height: 100vh; background-color: rgb(249, 250, 251);" class="notifications-page bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<!-- Success Message (hidden, used for HTMX responses) -->
|
||||
if data.SuccessMessage != "" {
|
||||
<div class="hidden success-message">{ data.SuccessMessage }</div>
|
||||
}
|
||||
<!-- Error Message (hidden, used for HTMX responses) -->
|
||||
if data.ErrorMessage != "" {
|
||||
<div class="hidden error-message">{ data.ErrorMessage }</div>
|
||||
}
|
||||
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||
Notification Services
|
||||
</h1>
|
||||
<a href="/admin/settings/notifications/new" class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||
Add Notification Service
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- List of Notification Services -->
|
||||
if len(data.NotificationServices) == 0 {
|
||||
<div class="text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4">
|
||||
<i class="fas fa-bell text-2xl text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No notification services configured</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">Add a notification service to receive alerts for job events.</p>
|
||||
<a href="/admin/settings/notifications/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||
Add First Notification Service
|
||||
</a>
|
||||
</div>
|
||||
} else {
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
for _, service := range data.NotificationServices {
|
||||
<li>
|
||||
<div class="block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
if service.Type == "email" {
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
} else if service.Type == "webhook" {
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3">
|
||||
<i class="fas fa-code"></i>
|
||||
</div>
|
||||
} else {
|
||||
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3">
|
||||
<i class="fas fa-bell"></i>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400 truncate">
|
||||
{ service.Name }
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{ service.Description }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2 flex-shrink-0 flex space-x-2">
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID)) }
|
||||
class="text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<!-- Add notification delete dialog -->
|
||||
@NotificationDialog(
|
||||
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
||||
"Delete Notification Service",
|
||||
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
||||
"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",
|
||||
"Delete",
|
||||
"delete",
|
||||
service.ID,
|
||||
service.Name,
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 sm:flex sm:justify-between">
|
||||
<div class="sm:flex flex-col md:flex-row gap-2 md:gap-6">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class={ "px-2 py-1 text-xs font-medium rounded-full",
|
||||
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
||||
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled) }
|
||||
>
|
||||
if service.IsEnabled {
|
||||
Active
|
||||
} else {
|
||||
Disabled
|
||||
}
|
||||
</span>
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full">
|
||||
{ service.Type }
|
||||
</span>
|
||||
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full">
|
||||
{ fmt.Sprintf("%d triggers", len(service.EventTriggers)) }
|
||||
</span>
|
||||
}
|
||||
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full">
|
||||
{ fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount + service.FailureCount) }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
if service.Type == "webhook" {
|
||||
<div class="mt-2 md:mt-0 flex items-center space-x-4">
|
||||
<div class="text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">Events:</span>
|
||||
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||
if len(service.EventTriggers) == 0 {
|
||||
None
|
||||
} else {
|
||||
for i, trigger := range service.EventTriggers {
|
||||
if i > 0 {
|
||||
<span>, </span>
|
||||
}
|
||||
{ trigger }
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">Retry:</span>
|
||||
<span class="ml-1 text-gray-900 dark:text-gray-300">
|
||||
if service.RetryPolicy == "" {
|
||||
Default
|
||||
} else {
|
||||
{ service.RetryPolicy }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<div class="mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<i class="far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
|
||||
<p>
|
||||
Last sent:
|
||||
if service.SuccessCount > 0 {
|
||||
"Recently"
|
||||
} else {
|
||||
"Never"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Help Notice -->
|
||||
<div class="mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-400 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||
Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('notifications-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('notifications-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package dialog
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// DialogScripts provides the JavaScript function specific to the notification delete confirmation dialog.
|
||||
func DialogScripts() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script type=\"text/javascript\">\n\t\t// Called when the delete confirmation button is clicked.\n\t\t// Primarily closes the modal; the actual delete is handled by hx-delete.\n\t\tfunction triggerServiceDelete(dialogId, serviceId, serviceName) {\n\t\t\tconsole.log(`Confirmed delete for service: ${serviceName} (ID: ${serviceId}). Closing modal: ${dialogId}`);\n\t\t\t// Call the global closeModal function defined elsewhere (e.g., app.js)\n\t\t\tif (typeof closeModal === 'function') {\n\t\t\t\tcloseModal(dialogId);\n\t\t\t} else {\n\t\t\t\tconsole.error('Global closeModal function not found.');\n\t\t\t}\n\t\t\t// Optional: Show a \"Deleting...\" toast here if desired.\n\t\t\t// The hx-delete attribute on the button will trigger the actual backend request.\n\t\t}\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,218 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package dialog
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
// "strconv" // No longer needed here
|
||||
)
|
||||
|
||||
// NotificationDialog component for confirmation dialogs using Flowbite modal
|
||||
func NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 10, Col: 13}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" 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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s-backdrop", id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 12, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if action == "delete" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"fas fa-trash-alt text-red-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<i class=\"fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h3 class=\"mb-5 text-lg font-normal text-gray-500 dark:text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 22, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 = []any{confirmClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button type=\"button\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/admin/settings/notifications/%d", serviceID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 26, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-target=\"body\" data-service-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(serviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 28, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-service-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(serviceID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 29, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-btn-%d", serviceID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 30, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("triggerServiceDelete('%s', %d, '%s')", id, serviceID, serviceName)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(confirmText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/dialog/dialog.templ`, Line: 32, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("closeModal('%s')", id)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" 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></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Scripts (triggerServiceDelete, closeModal, showModal) are now expected to be defined globally or in the calling template (e.g., list.templ).
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,45 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// No utils needed for this specific template yet
|
||||
)
|
||||
|
||||
func EmailFields(data types.NotificationFormData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- TODO: Populate value attributes if editing an email service --><div id=\"email_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"smtp_host\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Host</label> <input type=\"text\" id=\"smtp_host\" name=\"smtp_host\" 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 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=\"smtp.example.com\"></div><div class=\"mb-6\"><label for=\"smtp_port\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Port</label> <input type=\"number\" id=\"smtp_port\" name=\"smtp_port\" 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 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=\"587\"></div><div class=\"mb-6\"><label for=\"smtp_username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Username</label> <input type=\"text\" id=\"smtp_username\" name=\"smtp_username\" 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 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=\"user@example.com\"></div><div class=\"mb-6\"><label for=\"smtp_password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">SMTP Password</label> <input type=\"password\" id=\"smtp_password\" name=\"smtp_password\" 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 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\"></div><div class=\"mb-6\"><label for=\"from_email\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">From Email</label> <input type=\"email\" id=\"from_email\" name=\"from_email\" 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 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=\"notifications@example.com\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,154 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func GotifyFields(data types.NotificationFormData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"gotify_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"gotify_url\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Gotify Server URL</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"gotify_url\" name=\"gotify_url\" 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 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://gotify.example.com\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 13, Col: 407}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"gotify_url\" name=\"gotify_url\" 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 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://gotify.example.com\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">URL of your Gotify server</p></div><div class=\"mb-6\"><label for=\"gotify_token\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Application Token</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyToken != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"gotify_token\" name=\"gotify_token\" 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 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=\"A-M-XiEQj.zX5d\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 22, Col: 402}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"gotify_token\" name=\"gotify_token\" 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 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=\"A-M-XiEQj.zX5d\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Find this in your Gotify application settings</p></div><div class=\"mb-6\"><label for=\"gotify_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"gotify_priority\" name=\"gotify_priority\" 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 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\"><option value=\"0\">Low (0)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyPriority != "" && data.NotificationService.GotifyPriority == "5" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"5\" selected=\"selected\">Normal (5)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"5\">Normal (5)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<option value=\"8\">High (8)</option></select></div><div class=\"mb-6\"><label for=\"gotify_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<input type=\"text\" id=\"gotify_title_template\" name=\"gotify_title_template\" 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 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\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.GotifyTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/gotify.templ`, Line: 43, Col: 399}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<input type=\"text\" id=\"gotify_title_template\" name=\"gotify_title_template\" 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 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=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"gotify_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"gotify_message_template\" name=\"gotify_message_template\" rows=\"4\" 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 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=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.GotifyMessageTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "data.NotificationService.GotifyMessageTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Gotify --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-gotify-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Gotify configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,210 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func NtfyFields(data types.NotificationFormData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"ntfy_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"ntfy_server\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Ntfy Server</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyServer != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"ntfy_server\" name=\"ntfy_server\" 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 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://ntfy.sh\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyServer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 13, Col: 399}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"ntfy_server\" name=\"ntfy_server\" 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 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://ntfy.sh\" value=\"https://ntfy.sh\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">The Ntfy server URL (default: ntfy.sh)</p></div><div class=\"mb-6\"><label for=\"ntfy_topic\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Topic</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyTopic != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"ntfy_topic\" name=\"ntfy_topic\" 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 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-unique-topic\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyTopic)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 22, Col: 399}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"ntfy_topic\" name=\"ntfy_topic\" 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 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-unique-topic\" value=\"gomft\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Choose a unique, unguessable topic name</p></div><div class=\"mb-6\"><label for=\"ntfy_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"ntfy_priority\" name=\"ntfy_priority\" 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 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\"><option value=\"1\">Low (1)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyPriority == "3" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"3\" selected=\"selected\">Default (3)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"3\">Default (3)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<option value=\"4\">High (4)</option> <option value=\"5\">Urgent (5)</option></select></div><div class=\"mb-6\"><label for=\"ntfy_username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Username (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyUsername != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<input type=\"text\" id=\"ntfy_username\" name=\"ntfy_username\" 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 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 for protected topics\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyUsername)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 44, Col: 420}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<input type=\"text\" id=\"ntfy_username\" name=\"ntfy_username\" 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 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 for protected topics\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"ntfy_password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Password (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyPassword != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<input type=\"password\" id=\"ntfy_password\" name=\"ntfy_password\" 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 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=\"Password for protected topics\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyPassword)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 52, Col: 424}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"password\" id=\"ntfy_password\" name=\"ntfy_password\" 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 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=\"Password for protected topics\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"mb-6\"><label for=\"ntfy_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<input type=\"text\" id=\"ntfy_title_template\" name=\"ntfy_title_template\" 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 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\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.NtfyTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/ntfy.templ`, Line: 60, Col: 393}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<input type=\"text\" id=\"ntfy_title_template\" name=\"ntfy_title_template\" 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 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=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div class=\"mb-6\"><label for=\"ntfy_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"ntfy_message_template\" name=\"ntfy_message_template\" rows=\"4\" 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 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=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.NtfyMessageTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "data.NotificationService.NtfyMessageTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Ntfy --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-ntfy-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Ntfy configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,139 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func PushbulletFields(data types.NotificationFormData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"pushbullet_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"pushbullet_api_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">API Key</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletAPIKey != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"text\" id=\"pushbullet_api_key\" name=\"pushbullet_api_key\" 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 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=\"o.XyzAbCdEfGhIjKlMnOpQrSt\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletAPIKey)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 13, Col: 430}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"text\" id=\"pushbullet_api_key\" name=\"pushbullet_api_key\" 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 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=\"o.XyzAbCdEfGhIjKlMnOpQrSt\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Get your API key from <a href=\"https://www.pushbullet.com/#settings/account\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushbullet Account Settings</a></p></div><div class=\"mb-6\"><label for=\"pushbullet_device_iden\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Device Identifier (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletDeviceID != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"pushbullet_device_iden\" name=\"pushbullet_device_iden\" 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 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=\"Leave empty to send to all devices\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletDeviceID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 22, Col: 449}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"pushbullet_device_iden\" name=\"pushbullet_device_iden\" 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 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=\"Leave empty to send to all devices\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"mb-6\"><label for=\"pushbullet_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<input type=\"text\" id=\"pushbullet_title_template\" name=\"pushbullet_title_template\" 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 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\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushbulletTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushbullet.templ`, Line: 30, Col: 411}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"text\" id=\"pushbullet_title_template\" name=\"pushbullet_title_template\" 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 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=\"{{job.name}} {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"mb-6\"><label for=\"pushbullet_body_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"pushbullet_body_template\" name=\"pushbullet_body_template\" rows=\"4\" 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 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=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushbulletBodyTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "data.NotificationService.PushbulletBodyTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Pushbullet --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-pushbullet-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Pushbullet configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,182 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func PushoverFields(data types.NotificationFormData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"pushover_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"pushover_app_token\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">API Token/Key</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverAPIToken != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"text\" id=\"pushover_app_token\" name=\"pushover_app_token\" 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 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=\"azGDORePK8gMaC0QOYAMyEEuzJnyUi\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverAPIToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 13, Col: 435}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"text\" id=\"pushover_app_token\" name=\"pushover_app_token\" 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 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=\"azGDORePK8gMaC0QOYAMyEEuzJnyUi\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Your application's API token/key from <a href=\"https://pushover.net/apps\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushover Dashboard</a></p></div><div class=\"mb-6\"><label for=\"pushover_user_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">User Key</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverUserKey != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input type=\"text\" id=\"pushover_user_key\" name=\"pushover_user_key\" 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 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=\"uQiRzpo4DXghDmr9QzzfQu27cmVRsG\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverUserKey)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 22, Col: 432}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"text\" id=\"pushover_user_key\" name=\"pushover_user_key\" 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 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=\"uQiRzpo4DXghDmr9QzzfQu27cmVRsG\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Your user key from <a href=\"https://pushover.net/\" target=\"_blank\" class=\"text-blue-500 hover:underline\">Pushover Dashboard</a></p></div><div class=\"mb-6\"><label for=\"pushover_device\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Device Name (Optional)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverDevice != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<input type=\"text\" id=\"pushover_device\" name=\"pushover_device\" 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 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=\"Leave empty to send to all devices\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverDevice)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 31, Col: 431}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"text\" id=\"pushover_device\" name=\"pushover_device\" 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 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=\"Leave empty to send to all devices\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"mb-6\"><label for=\"pushover_priority\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Default Priority</label> <select id=\"pushover_priority\" name=\"pushover_priority\" 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 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\"><option value=\"-2\">Lowest (-2)</option> <option value=\"-1\">Low (-1)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverPriority != "" && data.NotificationService.PushoverPriority == "0" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<option value=\"0\" selected=\"selected\">Normal (0)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<option value=\"0\">Normal (0)</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"1\">High (1)</option> <option value=\"2\">Emergency (2)</option></select></div><div class=\"mb-6\"><label for=\"pushover_sound\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Sound</label> <select id=\"pushover_sound\" name=\"pushover_sound\" 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 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\"><option value=\"pushover\">Pushover (default)</option> <option value=\"bike\">Bike</option> <option value=\"bugle\">Bugle</option> <option value=\"cashregister\">Cash Register</option> <option value=\"classical\">Classical</option> <option value=\"cosmic\">Cosmic</option> <option value=\"falling\">Falling</option> <option value=\"gamelan\">Gamelan</option> <option value=\"incoming\">Incoming</option> <option value=\"intermission\">Intermission</option> <option value=\"magic\">Magic</option> <option value=\"mechanical\">Mechanical</option> <option value=\"pianobar\">Piano Bar</option> <option value=\"siren\">Siren</option> <option value=\"spacealarm\">Space Alarm</option> <option value=\"tugboat\">Tug Boat</option> <option value=\"alien\">Alien Alarm (long)</option> <option value=\"climb\">Climb (long)</option> <option value=\"persistent\">Persistent (long)</option> <option value=\"echo\">Echo (long)</option> <option value=\"updown\">Up Down (long)</option> <option value=\"vibrate\">Vibrate Only</option> <option value=\"none\">None (silent)</option></select></div><div class=\"mb-6\"><label for=\"pushover_title_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Title Template</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverTitleTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<input type=\"text\" id=\"pushover_title_template\" name=\"pushover_title_template\" 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 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\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.PushoverTitleTemplate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/pushover.templ`, Line: 81, Col: 405}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"text\" id=\"pushover_title_template\" name=\"pushover_title_template\" 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 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=\"'{{job.name}}' {{job.status}}\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"mb-6\"><label for=\"pushover_message_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Message Body Template</label> <textarea id=\"pushover_message_template\" name=\"pushover_message_template\" rows=\"4\" 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 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=\"Job '{{job.name}}' {{job.status}} at {{job.completed_at}}. {{job.file_count}} files transferred ({{job.transfer_bytes}} bytes).\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PushoverMessageTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "data.NotificationService.PushoverMessageTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><!-- Test notification button for Pushover --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-pushover-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your Pushover configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,223 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package fields
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
// "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
|
||||
)
|
||||
|
||||
func WebhookFields(data types.NotificationFormData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"webhook_fields\" class=\"hidden notification-fields\"><div class=\"mb-6\"><label for=\"webhook_url\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Webhook URL</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.WebhookURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<input type=\"url\" id=\"webhook_url\" name=\"webhook_url\" 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 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://api.example.com/webhook\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.WebhookURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 13, Col: 415}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"url\" id=\"webhook_url\" name=\"webhook_url\" 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 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://api.example.com/webhook\" value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"mb-6\"><label for=\"method\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">HTTP Method</label> <select id=\"method\" name=\"method\" 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 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Method != "" {
|
||||
if data.NotificationService.Method == "POST" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<option value=\"POST\" selected=\"selected\">POST</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<option value=\"POST\">POST</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Method == "PUT" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<option value=\"PUT\" selected=\"selected\">PUT</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<option value=\"PUT\">PUT</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"POST\">POST</option> <option value=\"PUT\">PUT</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</select></div><div class=\"mb-6\"><label for=\"headers\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Headers (JSON)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Headers != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<textarea id=\"headers\" name=\"headers\" rows=\"3\" 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 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=\"{"Content-Type": "application/json", "Authorization": "Bearer token"}\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Headers)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 41, Col: 437}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<textarea id=\"headers\" name=\"headers\" rows=\"3\" 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 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=\"{"Content-Type": "application/json", "Authorization": "Bearer token"}\"></textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"mb-6\"><label for=\"payload_template\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Payload Template (JSON)</label> <textarea id=\"payload_template\" name=\"payload_template\" rows=\"5\" 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 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=\"{\n\t"event": "{{job.event}}",\n\t"job": {\n\t\t\t"id": "{{job.id}}",\n\t\t\t"name": "{{job.name}}",\n\t\t\t"status": "{{job.status}}",\n\t\t\t"message": "{{job.message}}",\n\t\t\t"started_at": "{{job.started_at}}",\n\t\t\t"completed_at": "{{job.completed_at}}",\n\t\t\t"duration_seconds": {{job.duration_seconds}},\n\t\t\t"config_id": "{{job.config_id}}",\n\t\t\t"config_name": "{{job.config_name}}",\n\t\t\t"transfer_bytes": {{job.transfer_bytes}},\n\t\t\t"file_count": {{job.file_count}}\n\t},\n\t"instance": {\n\t\t\t"id": "{{instance.id}}",\n\t\t\t"name": "{{instance.name}}",\n\t\t\t"version": "{{instance.version}}",\n\t\t\t"environment": "{{instance.environment}}"\n\t},\n\t"timestamp": "{{timestamp}}",\n\t"notification_id": "{{notification.id}}"\n}\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.PayloadTemplate != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "data.NotificationService.PayloadTemplate")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea><p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*</p></div><div class=\"mb-6\"><label for=\"secret_key\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Secret Key (for signature verification)</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.SecretKey != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<input type=\"text\" id=\"secret_key\" name=\"secret_key\" 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 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=\"Optional signature verification key\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.SecretKey)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/fields/webhook.templ`, Line: 87, Col: 417}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">If provided, all webhooks will include an X-GoMFT-Signature header</p></div><div class=\"mb-6\"><label for=\"retry_policy\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Retry Policy</label> <select id=\"retry_policy\" name=\"retry_policy\" 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 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.RetryPolicy != "" {
|
||||
if data.NotificationService.RetryPolicy == "none" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<option value=\"none\" selected=\"selected\">No retries</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"none\">No retries</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "simple" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<option value=\"simple\" selected=\"selected\">Simple (3 retries)</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<option value=\"simple\">Simple (3 retries)</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.RetryPolicy == "exponential" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<option value=\"exponential\" selected=\"selected\">Exponential backoff</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<option value=\"exponential\">Exponential backoff</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<option value=\"none\">No retries</option> <option value=\"simple\">Simple (3 retries)</option> <option value=\"exponential\">Exponential backoff</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</select></div><!-- Test notification button --><div class=\"mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600\"><div class=\"flex items-center justify-between mb-2\"><h4 class=\"text-base font-medium text-gray-900 dark:text-white\">Test Configuration</h4><button type=\"button\" id=\"test-webhook-btn\" hx-post=\"/admin/settings/notifications/test\" hx-trigger=\"click\" hx-target=\"#test-notification-result\" hx-swap=\"outerHTML\" class=\"px-3 py-2 text-xs font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-paper-plane mr-1\"></i> Send Test Notification</button></div><p class=\"text-sm text-gray-500 dark:text-gray-400\">Send a test notification to verify your configuration works correctly before saving.</p><div id=\"test-notification-result\" class=\"mt-3 hidden\"><!-- Result will be shown here --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -10,8 +10,48 @@ import (
|
||||
)
|
||||
|
||||
templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
|
||||
@FormScripts() // Include the form-specific scripts
|
||||
@components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx) {
|
||||
<script>
|
||||
// Toggle notification fields based on selection
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const typeSelector = document.getElementById('notification_type');
|
||||
// Ensure typeSelector exists before adding listener
|
||||
if (!typeSelector) {
|
||||
console.warn("Notification type selector not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const allFields = document.querySelectorAll('.notification-fields');
|
||||
const commonFields = document.querySelectorAll('.common-fields');
|
||||
|
||||
function toggleFields() {
|
||||
// Hide all specific fields first
|
||||
allFields.forEach(field => field.classList.add('hidden'));
|
||||
|
||||
// Show/hide common fields based on selection
|
||||
const selectedType = typeSelector.value;
|
||||
if (selectedType) {
|
||||
// Show common fields (name, description, is_enabled, submit)
|
||||
commonFields.forEach(field => field.classList.remove('hidden'));
|
||||
|
||||
// Show the selected type's specific fields
|
||||
const fieldsToShow = document.getElementById(`${selectedType}_fields`);
|
||||
if (fieldsToShow) {
|
||||
fieldsToShow.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
// Hide common fields if no type selected
|
||||
commonFields.forEach(field => field.classList.add('hidden'));
|
||||
}
|
||||
}
|
||||
|
||||
typeSelector.addEventListener('change', toggleFields);
|
||||
|
||||
// Initialize form state on load (if editing or if a type is pre-selected)
|
||||
toggleFields();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Status and Error Messages (Handled by shared toast component in layout) -->
|
||||
|
||||
<div id="notification-form-container" class="notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||
@@ -41,10 +81,12 @@ templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
|
||||
<form id="notification-form"
|
||||
if data.IsNew {
|
||||
hx-post="/admin/settings/notifications"
|
||||
hx-redirect="/admin/settings/notifications"
|
||||
} else {
|
||||
hx-put={ fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID) }
|
||||
hx-redirect="/admin/settings/notifications"
|
||||
}
|
||||
hx-target="#notification-form-container">
|
||||
hx-target="body">
|
||||
<div class="mb-6">
|
||||
<label for="notification_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Notification Type</label>
|
||||
<select id="notification_type" name="type" 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 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">
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package form
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// FormScripts contains JavaScript specific to the notification form page.
|
||||
func FormScripts() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t// Toggle notification fields based on selection\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\tconst typeSelector = document.getElementById('notification_type');\n\t\t\t// Ensure typeSelector exists before adding listener\n\t\t\tif (!typeSelector) {\n\t\t\t\tconsole.warn(\"Notification type selector not found.\");\n\t\t\t\treturn; \n\t\t\t}\n\n\t\t\tconst allFields = document.querySelectorAll('.notification-fields');\n\t\t\tconst commonFields = document.querySelectorAll('.common-fields');\n\n\t\t\tfunction toggleFields() {\n\t\t\t\t// Hide all specific fields first\n\t\t\t\tallFields.forEach(field => field.classList.add('hidden'));\n\n\t\t\t\t// Show/hide common fields based on selection\n\t\t\t\tconst selectedType = typeSelector.value;\n\t\t\t\tif (selectedType) {\n\t\t\t\t\t// Show common fields (name, description, is_enabled, submit)\n\t\t\t\t\tcommonFields.forEach(field => field.classList.remove('hidden'));\n\n\t\t\t\t\t// Show the selected type's specific fields\n\t\t\t\t\tconst fieldsToShow = document.getElementById(`${selectedType}_fields`);\n\t\t\t\t\tif (fieldsToShow) {\n\t\t\t\t\t\tfieldsToShow.classList.remove('hidden');\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Hide common fields if no type selected\n\t\t\t\t\tcommonFields.forEach(field => field.classList.add('hidden'));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttypeSelector.addEventListener('change', toggleFields);\n\n\t\t\t// Initialize form state on load (if editing or if a type is pre-selected)\n\t\t\ttoggleFields(); \n\t\t});\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,398 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package form
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components" // For LayoutWithContext
|
||||
"github.com/starfleetcptn/gomft/components/notifications/form/fields" // Import fields
|
||||
"github.com/starfleetcptn/gomft/components/notifications/form/utils"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
)
|
||||
|
||||
func NotificationForm(ctx context.Context, data types.NotificationFormData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = FormScripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages (Handled by shared toast component in layout) --> <div id=\"notification-form-container\" class=\"notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen\"><div class=\"pb-8 w-full max-w-4xl mx-auto\"><!-- Success Message (hidden, used for HTMX responses) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.SuccessMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hidden success-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.SuccessMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 21, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Error Message (hidden, used for HTMX responses) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.ErrorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"hidden error-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 25, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400\"></i> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(utils.GetNotificationFormTitle(data.IsNew))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 31, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h1><a href=\"/admin/settings/notifications\" class=\"flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700\"><i class=\"fas fa-arrow-left w-4 h-4 mr-2\"></i> Back to Notification Services</a></div><!-- Add Notification Service Form --><div class=\"mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800\"><form id=\"notification-form\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.IsNew {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " hx-post=\"/admin/settings/notifications\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " hx-put=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/admin/settings/notifications/%d", data.NotificationService.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 45, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " hx-target=\"#notification-form-container\"><div class=\"mb-6\"><label for=\"notification_type\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Notification Type</label> <select id=\"notification_type\" name=\"type\" 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 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\"><option value=\"\">Select a type</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<option value=\"webhook\" selected=\"selected\">Webhook</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<option value=\"webhook\">Webhook</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushbullet" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<option value=\"pushbullet\" selected=\"selected\">Pushbullet</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"pushbullet\">Pushbullet</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "ntfy" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<option value=\"ntfy\" selected=\"selected\">Ntfy</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<option value=\"ntfy\">Ntfy</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "gotify" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<option value=\"gotify\" selected=\"selected\">Gotify</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<option value=\"gotify\">Gotify</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.Type == "pushover" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<option value=\"pushover\" selected=\"selected\">Pushover</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<option value=\"pushover\">Pushover</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"email\" disabled>Email (Coming Soon)</option></select></div><div class=\"mb-6 hidden common-fields\"><label for=\"notification_name\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Name</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Name != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<input type=\"text\" id=\"notification_name\" name=\"name\" 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 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 Notification Service\" required value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 83, Col: 414}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<input type=\"text\" id=\"notification_name\" name=\"name\" 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 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 Notification Service\" required value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div><div class=\"mb-6 hidden common-fields\"><label for=\"notification_description\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Description</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService.Description != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<textarea id=\"notification_description\" name=\"description\" rows=\"3\" 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 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=\"Description for this notification service\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.NotificationService.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 91, Col: 438}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<textarea id=\"notification_description\" name=\"description\" rows=\"3\" 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 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=\"Description for this notification service\"></textarea>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><!-- Dynamic fields based on notification type -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.EmailFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.WebhookFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.PushbulletFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.NtfyFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.GotifyFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = fields.PushoverFields(data).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"mb-6 hidden common-fields\"><label class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Event Triggers</label><p class=\"text-xs text-gray-500 dark:text-gray-400 mb-2\">Select the job events that should trigger this notification.</p><div class=\"flex flex-wrap gap-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, event := range []string{"job_start", "job_complete", "job_error"} {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"flex items-center\"><input id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("trigger_" + event)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 113, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" name=\"event_triggers[]\" type=\"checkbox\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(event)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 116, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if utils.IsEventTriggerSelected(data.NotificationService, event, data.IsNew) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("trigger_" + event)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 122, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" class=\"ml-2 text-sm font-medium text-gray-900 dark:text-gray-300\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatEventTriggerName(event))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 122, Col: 147}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</label></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></div><div class=\"flex items-start mb-6 hidden common-fields\"><div class=\"flex items-center h-5\"><input id=\"is_enabled\" name=\"is_enabled\" type=\"checkbox\" value=\"true\" class=\"w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.NotificationService != nil && data.NotificationService.IsEnabled {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "></div><div class=\"ml-3 text-sm\"><label for=\"is_enabled\" class=\"font-medium text-gray-900 dark:text-white\">Enable this notification service</label><p class=\"text-xs text-gray-500 dark:text-gray-400\">Check this box to make the service active.</p></div></div><div class=\"hidden common-fields\"><button type=\"submit\" class=\"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.IsNew {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "Add Service")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "Save Changes")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</button></div></form></div><!-- Help Notice --><div class=\"mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700\"><div class=\"flex\"><div class=\"flex-shrink-0\"><i class=\"fas fa-info-circle text-blue-400 dark:text-blue-400\"></i></div><div class=\"ml-3\"><p class=\"text-sm text-blue-700 dark:text-blue-400\">Configure your notification service to receive alerts for job events. Different notification types have different configuration options.</p></div></div></div></div></div><!-- Theme-specific background handled by Tailwind classes -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext(utils.GetNotificationFormTitle(data.IsNew), ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
File diff suppressed because one or more lines are too long
@@ -1,403 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package list
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/dialog"
|
||||
"github.com/starfleetcptn/gomft/components/notifications/types"
|
||||
)
|
||||
|
||||
// List renders the notification services list page.
|
||||
func List(ctx context.Context, data types.SettingsNotificationsData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Status and Error Messages (Handled by shared toast component in layout) --> <div id=\"notifications-container\" class=\"notifications-page bg-gray-50 dark:bg-gray-900 min-h-screen\"><div class=\"pb-8 w-full\"><!-- Success Message (hidden, used for HTMX responses/toast trigger) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.SuccessMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hidden success-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.SuccessMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 20, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Error Message (hidden, used for HTMX responses/toast trigger) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.ErrorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"hidden error-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 24, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-white flex items-center\"><i class=\"fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400\"></i> Notification Services</h1><a href=\"/admin/settings/notifications/new\" class=\"flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800\"><i class=\"fas fa-plus w-4 h-4 mr-2\"></i> Add Notification Service</a></div><!-- List of Notification Services -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data.NotificationServices) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"text-center py-8 bg-white dark:bg-gray-800 shadow-md rounded-lg\"><div class=\"inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 mb-4\"><i class=\"fas fa-bell text-2xl text-blue-600 dark:text-blue-400\"></i></div><h3 class=\"mb-2 text-lg font-semibold text-gray-900 dark:text-white\">No notification services configured</h3><p class=\"text-gray-500 dark:text-gray-400 mb-4\">Add a notification service to receive alerts for job events.</p><a href=\"/admin/settings/notifications/new\" class=\"inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800\"><i class=\"fas fa-plus w-4 h-4 mr-2\"></i> Add First Notification Service</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden\"><ul class=\"divide-y divide-gray-200 dark:divide-gray-700\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, service := range data.NotificationServices {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<li><div class=\"block hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\"><div class=\"px-4 py-4 sm:px-6\"><div class=\"flex items-center justify-between\"><div class=\"flex items-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.Type == "email" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 dark:bg-blue-900 dark:text-blue-400 mr-3\"><i class=\"fas fa-envelope\"></i></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if service.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 dark:bg-green-900 dark:text-green-400 mr-3\"><i class=\"fas fa-code\"></i></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " <div class=\"w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-gray-700 dark:text-gray-400 mr-3\"><i class=\"fas fa-bell\"></i></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div><p class=\"text-sm font-medium text-blue-600 dark:text-blue-400 truncate\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(service.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 75, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p><p class=\"text-sm text-gray-500 dark:text-gray-400 mt-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(service.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 78, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></div></div><div class=\"ml-2 flex-shrink-0 flex space-x-2\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/admin/settings/notifications/%d/edit", service.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"text-gray-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 mr-1 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700\"><i class=\"fas fa-edit\"></i></a><!-- Add notification delete dialog -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dialog.NotificationDialog(
|
||||
fmt.Sprintf("delete-notification-dialog-%d", service.ID),
|
||||
"Delete Notification Service",
|
||||
fmt.Sprintf("Are you sure you want to delete the notification service '%s'? This cannot be undone.", service.Name),
|
||||
"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",
|
||||
"Delete",
|
||||
"delete",
|
||||
service.ID,
|
||||
service.Name,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)})
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<button type=\"button\" onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showModal('delete-notification-dialog-%d')", service.ID)}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" 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></button></div></div><div class=\"mt-3 sm:flex sm:justify-between\"><div class=\"sm:flex flex-col md:flex-row gap-2 md:gap-6\"><div class=\"flex items-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 = []any{"px-2 py-1 text-xs font-medium rounded-full",
|
||||
templ.KV("bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", service.IsEnabled),
|
||||
templ.KV("bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", !service.IsEnabled)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.IsEnabled {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Active")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Disabled")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span> <span class=\"ml-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 rounded-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(service.Type)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 124, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(service.EventTriggers) > 0 && service.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"ml-2 px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300 rounded-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d triggers", len(service.EventTriggers)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 128, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if service.SuccessCount > 0 || service.FailureCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span class=\"ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 rounded-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", service.SuccessCount, service.SuccessCount+service.FailureCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 133, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.Type == "webhook" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<div class=\"mt-2 md:mt-0 flex items-center space-x-4\"><div class=\"text-xs\"><span class=\"text-gray-500 dark:text-gray-400\">Events:</span> <span class=\"ml-1 text-gray-900 dark:text-gray-300\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(service.EventTriggers) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "None")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
for i, trigger := range service.EventTriggers {
|
||||
if i > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<span>, </span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(trigger)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 150, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></div><div class=\"text-xs\"><span class=\"text-gray-500 dark:text-gray-400\">Retry:</span> <span class=\"ml-1 text-gray-900 dark:text-gray-300\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.RetryPolicy == "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Default")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(service.RetryPolicy)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/list/list.templ`, Line: 161, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"mt-2 md:mt-0 flex items-center text-sm text-gray-500 dark:text-gray-400\"><i class=\"far fa-clock w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500\"></i><p>Last sent: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if service.SuccessCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"Recently\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"Never\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div></div></div></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</ul></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<!-- Help Notice Placeholder --><div class=\"mt-8 p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700\"><div class=\"flex\"><div class=\"flex-shrink-0\"><i class=\"fas fa-info-circle text-blue-400 dark:text-blue-400\"></i></div><div class=\"ml-3\"><p class=\"text-sm text-blue-700 dark:text-blue-400\">Notification services allow the system to send alerts for job events such as completion, errors, or when jobs start.</p></div></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = dialog.DialogScripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.LayoutWithContext("Notification Services", ctx).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -195,12 +195,15 @@ templ SourceSelection() {
|
||||
<option value="sftp">SFTP</option>
|
||||
<option value="ftp">FTP</option>
|
||||
<option value="s3">S3</option>
|
||||
<option value="b2">Backblaze B2</option>
|
||||
<option value="wasabi">Wasabi</option>
|
||||
<option value="minio">MinIO</option>
|
||||
<option value="smb">SMB</option>
|
||||
<option value="nextcloud">NextCloud</option>
|
||||
<option value="webdav">WebDAV</option>
|
||||
<option value="gdrive">Google Drive (BETA)</option>
|
||||
<option value="gphotos">Google Photos (BETA)</option>
|
||||
<option value="hetzner">Hetzner Storage Box</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,12 +222,15 @@ templ DestinationSelection() {
|
||||
<option value="sftp">SFTP</option>
|
||||
<option value="ftp">FTP</option>
|
||||
<option value="s3">S3</option>
|
||||
<option value="b2">Backblaze B2</option>
|
||||
<option value="wasabi">Wasabi</option>
|
||||
<option value="minio">MinIO</option>
|
||||
<option value="smb">SMB</option>
|
||||
<option value="nextcloud">NextCloud</option>
|
||||
<option value="webdav">WebDAV</option>
|
||||
<option value="gdrive">Google Drive (BETA)</option>
|
||||
<option value="gphotos">Google Photos (BETA)</option>
|
||||
<option value="hetzner">Hetzner Storage Box</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package destination
|
||||
|
||||
templ B2DestinationForm() {
|
||||
<div class="space-y-6 mt-4">
|
||||
<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">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Configure your Backblaze B2 details below. You'll need your account ID, application key, and bucket information.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||
<div class="relative">
|
||||
<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="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-b2-bucket" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Name of your Backblaze B2 bucket (case-sensitive)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Account ID</label>
|
||||
<div class="relative">
|
||||
<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_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="0123456789abcdef0123456789" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Backblaze B2 Account ID
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Key</label>
|
||||
<div class="relative">
|
||||
<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="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="K0123456789abcdef0123456789abcdef0123456789" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Backblaze B2 Application Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</label>
|
||||
<div class="relative">
|
||||
<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="destEndpoint"
|
||||
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="s3.us-west-000.backblazeb2.com" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Custom endpoint URL (only needed for non-standard regions or private endpoints)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||
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/files/" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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-shield-alt mr-2 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium">Security Note</h3>
|
||||
<p class="mt-1">It's recommended to create an application key with restricted permissions for this configuration. The application key should only have access to the specific B2 bucket and operations needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package destination
|
||||
|
||||
templ HetznerDestinationForm() {
|
||||
<div class="space-y-6 mt-4">
|
||||
<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">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Configure your Hetzner Storage Box details below. You'll need your server details, username, and password.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Storage Box Host</label>
|
||||
<div class="relative">
|
||||
<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="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="uXXXXXX.your-storagebox.de" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Hetzner Storage Box hostname (e.g., uXXXXXX.your-storagebox.de)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-plug text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="number" id="dest_port" name="dest_port" x-model="destPort" 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="23" min="1" max="65535" value="23" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Connection port (default: 23)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_auth_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authentication Type</label>
|
||||
<div class="flex">
|
||||
<div class="flex items-center me-4">
|
||||
<input id="dest_auth_password" type="radio" value="password" name="dest_auth_type" x-model="destAuthType"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="dest_auth_password" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
</div>
|
||||
<div class="flex items-center me-4">
|
||||
<input id="dest_auth_key" type="radio" value="key" name="dest_auth_type" x-model="destAuthType"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="dest_auth_key" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||
<div class="relative">
|
||||
<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="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="uXXXXXX" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Hetzner Storage Box username (typically matches your Storage Box number)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div x-show="destAuthType === 'password'" class="mb-6">
|
||||
<label for="dest_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<div class="relative">
|
||||
<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="destPassword" x-bind:required="requiresDestination && destAuthType === 'password'"
|
||||
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 password" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Hetzner Storage Box password
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div x-show="destAuthType === 'key'" class="mb-6">
|
||||
<label for="dest_key_file" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key File</label>
|
||||
<div class="relative">
|
||||
<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="destKeyFile" x-bind:required="requiresDestination && destAuthType === 'key'"
|
||||
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" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path to your SSH private key file (must be readable by the application)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" 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="/backups" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path on your Storage Box (e.g., /backups, /path/to/files)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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-shield-alt mr-2 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium">Security Note</h3>
|
||||
<p class="mt-1">For increased security, consider using SSH key authentication instead of password authentication when possible.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -27,15 +27,5 @@ templ LocalDestinationForm() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
@click="checkPath(destinationPath, 'dest')"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2 inline" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-6.5 4.5A5.5 5.5 0 0 1 9 9h2a5.5 5.5 0 0 1 5.5 5.5V17a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-2.5Z"/>
|
||||
</svg>
|
||||
Check Location
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ templ NextCloudDestinationForm() {
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">NextCloud URL</label>
|
||||
<label for="dest_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">NextCloud URL</label>
|
||||
<div class="relative">
|
||||
<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="destEndpoint" 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="https://nextcloud.example.com" />
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@ templ NextCloudDestinationForm() {
|
||||
</div>
|
||||
<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" />
|
||||
placeholder="username" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your NextCloud username
|
||||
@@ -47,7 +47,7 @@ templ NextCloudDestinationForm() {
|
||||
</div>
|
||||
<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" />
|
||||
placeholder="password" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your NextCloud account password
|
||||
@@ -62,10 +62,10 @@ templ NextCloudDestinationForm() {
|
||||
</div>
|
||||
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" 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="remote.php/dav/files/username/path/to/files" />
|
||||
placeholder="/path/to/files" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path to your files in NextCloud. Usually starts with "remote.php/dav/files/username/"
|
||||
Path to your files in NextCloud
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package destination
|
||||
|
||||
templ WasabiDestinationForm() {
|
||||
<div class="space-y-6 mt-4">
|
||||
<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">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Configure your Wasabi Cloud Storage details below. You'll need your access key, secret key, region, and bucket information.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Wasabi Region</label>
|
||||
<div class="relative">
|
||||
<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="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>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Wasabi region where your bucket is located (e.g., us-east-1, us-west-1, eu-central-1)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||
<div class="relative">
|
||||
<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="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-wasabi-bucket" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Name of your Wasabi bucket (case-sensitive)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</label>
|
||||
<div class="relative">
|
||||
<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="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="ABCDEFGHIJKLMNOPQRST" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Wasabi Access Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</label>
|
||||
<div class="relative">
|
||||
<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="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="abcdefghijklmnopqrstuvwxyz1234567890ABCD" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Wasabi Secret Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</label>
|
||||
<div class="relative">
|
||||
<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="destEndpoint"
|
||||
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="s3.wasabisys.com" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Wasabi endpoint URL (usually s3.wasabisys.com or region-specific endpoints)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||
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/files/" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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-shield-alt mr-2 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium">Security Note</h3>
|
||||
<p class="mt-1">It's recommended to use an IAM user with restricted permissions for this configuration. The IAM user should only have access to the specific Wasabi bucket and operations needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -10,12 +10,12 @@ templ WebDAVDestinationForm() {
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">WebDAV URL</label>
|
||||
<label for="dest_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">WebDAV URL</label>
|
||||
<div class="relative">
|
||||
<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="destEndpoint" 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="https://webdav.example.com" />
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@ templ WebDAVDestinationForm() {
|
||||
</div>
|
||||
<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" />
|
||||
placeholder="username" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your WebDAV username
|
||||
@@ -47,7 +47,7 @@ templ WebDAVDestinationForm() {
|
||||
</div>
|
||||
<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" />
|
||||
placeholder="password" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your WebDAV account password
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package source
|
||||
|
||||
templ B2SourceForm() {
|
||||
<div class="space-y-6 mt-4">
|
||||
<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">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Configure your Backblaze B2 details below. You'll need your account ID, application key, and bucket information.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||
<div class="relative">
|
||||
<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="source_bucket" name="source_bucket" x-model="sourceBucket" required
|
||||
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-b2-bucket" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Name of your Backblaze B2 bucket (case-sensitive)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Account ID</label>
|
||||
<div class="relative">
|
||||
<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="source_access_key" name="source_access_key" x-model="sourceAccessKey" required
|
||||
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="0123456789abcdef0123456789" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Backblaze B2 Account ID
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Key</label>
|
||||
<div class="relative">
|
||||
<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="source_secret_key" name="source_secret_key" x-model="sourceSecretKey" required
|
||||
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="K0123456789abcdef0123456789abcdef0123456789" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Backblaze B2 Application Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</label>
|
||||
<div class="relative">
|
||||
<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="source_endpoint" name="source_endpoint" x-model="sourceEndpoint"
|
||||
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="s3.us-west-000.backblazeb2.com" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Custom endpoint URL (only needed for non-standard regions or private endpoints)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||
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/files/" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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-shield-alt mr-2 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium">Security Note</h3>
|
||||
<p class="mt-1">It's recommended to create an application key with restricted permissions for this configuration. The application key should only have access to the specific B2 bucket and operations needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package source
|
||||
|
||||
templ HetznerSourceForm() {
|
||||
<div class="space-y-6 mt-4">
|
||||
<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">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Configure your Hetzner Storage Box details below. You'll need your server details, username, and password.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Storage Box Host</label>
|
||||
<div class="relative">
|
||||
<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="source_host" name="source_host" x-model="sourceHost" required
|
||||
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="uXXXXXX.your-storagebox.de" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Hetzner Storage Box hostname (e.g., uXXXXXX.your-storagebox.de)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-plug text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="number" id="source_port" name="source_port" x-model="sourcePort" required
|
||||
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="23" min="1" max="65535" value="23" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Connection port (default: 23)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_auth_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authentication Type</label>
|
||||
<div class="flex">
|
||||
<div class="flex items-center me-4">
|
||||
<input id="source_auth_password" type="radio" value="password" name="source_auth_type" x-model="sourceAuthType"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="source_auth_password" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
</div>
|
||||
<div class="flex items-center me-4">
|
||||
<input id="source_auth_key" type="radio" value="key" name="source_auth_type" x-model="sourceAuthType"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="source_auth_key" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||
<div class="relative">
|
||||
<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="source_user" name="source_user" x-model="sourceUser" required
|
||||
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="uXXXXXX" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Hetzner Storage Box username (typically matches your Storage Box number)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div x-show="sourceAuthType === 'password'" class="mb-6">
|
||||
<label for="source_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<div class="relative">
|
||||
<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="source_password" name="source_password" x-model="sourcePassword" x-bind:required="sourceAuthType === 'password'"
|
||||
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 password" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Hetzner Storage Box password
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div x-show="sourceAuthType === 'key'" class="mb-6">
|
||||
<label for="source_key_file" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key File</label>
|
||||
<div class="relative">
|
||||
<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="source_key_file" name="source_key_file" x-model="sourceKeyFile" x-bind:required="sourceAuthType === 'key'"
|
||||
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" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path to your SSH private key file (must be readable by the application)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="source_path" name="source_path" x-model="sourcePath" required
|
||||
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="/backups" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path on your Storage Box (e.g., /backups, /path/to/files)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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-shield-alt mr-2 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium">Security Note</h3>
|
||||
<p class="mt-1">For increased security, consider using SSH key authentication instead of password authentication when possible.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -27,15 +27,5 @@ templ LocalSourceForm() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
@click="checkPath(sourcePath, 'source')"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2 inline" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-6.5 4.5A5.5 5.5 0 0 1 9 9h2a5.5 5.5 0 0 1 5.5 5.5V17a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-2.5Z"/>
|
||||
</svg>
|
||||
Check Location
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ templ NextCloudSourceForm() {
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">NextCloud URL</label>
|
||||
<label for="source_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">NextCloud URL</label>
|
||||
<div class="relative">
|
||||
<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="source_endpoint" name="source_endpoint" x-model="sourceEndpoint" required
|
||||
<input type="text" id="source_host" name="source_host" x-model="sourceHost" required
|
||||
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>
|
||||
@@ -32,7 +32,7 @@ templ NextCloudSourceForm() {
|
||||
</div>
|
||||
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
|
||||
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" />
|
||||
placeholder="username" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your NextCloud username
|
||||
@@ -47,7 +47,7 @@ templ NextCloudSourceForm() {
|
||||
</div>
|
||||
<input type="password" id="source_password" name="source_password" x-model="sourcePassword" required
|
||||
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" />
|
||||
placeholder="password" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your NextCloud account password
|
||||
@@ -62,10 +62,10 @@ templ NextCloudSourceForm() {
|
||||
</div>
|
||||
<input type="text" id="source_path" name="source_path" x-model="sourcePath" required
|
||||
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="remote.php/dav/files/username/path/to/files" />
|
||||
placeholder="/path/to/files" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path to your files in NextCloud. Usually starts with "remote.php/dav/files/username/"
|
||||
Path to your files in NextCloud
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package source
|
||||
|
||||
templ WasabiSourceForm() {
|
||||
<div class="space-y-6 mt-4">
|
||||
<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">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span>Configure your Wasabi Cloud Storage details below. You'll need your access key, secret key, region, and bucket information.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Wasabi Region</label>
|
||||
<div class="relative">
|
||||
<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="source_region" name="source_region" x-model="sourceRegion" required
|
||||
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>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Wasabi region where your bucket is located (e.g., us-east-1, us-west-1, eu-central-1)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</label>
|
||||
<div class="relative">
|
||||
<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="source_bucket" name="source_bucket" x-model="sourceBucket" required
|
||||
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-wasabi-bucket" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Name of your Wasabi bucket (case-sensitive)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</label>
|
||||
<div class="relative">
|
||||
<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="source_access_key" name="source_access_key" x-model="sourceAccessKey" required
|
||||
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="ABCDEFGHIJKLMNOPQRST" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Wasabi Access Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</label>
|
||||
<div class="relative">
|
||||
<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="source_secret_key" name="source_secret_key" x-model="sourceSecretKey" required
|
||||
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="abcdefghijklmnopqrstuvwxyz1234567890ABCD" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your Wasabi Secret Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</label>
|
||||
<div class="relative">
|
||||
<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="source_endpoint" name="source_endpoint" x-model="sourceEndpoint"
|
||||
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="s3.wasabisys.com" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Wasabi endpoint URL (usually s3.wasabisys.com or region-specific endpoints)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
|
||||
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||
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/files/" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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-shield-alt mr-2 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h3 class="font-medium">Security Note</h3>
|
||||
<p class="mt-1">It's recommended to use an IAM user with restricted permissions for this configuration. The IAM user should only have access to the specific Wasabi bucket and operations needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -10,12 +10,12 @@ templ WebDAVSourceForm() {
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">WebDAV URL</label>
|
||||
<label for="source_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">WebDAV URL</label>
|
||||
<div class="relative">
|
||||
<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="source_endpoint" name="source_endpoint" x-model="sourceEndpoint" required
|
||||
<input type="text" id="source_host" name="source_host" x-model="sourceHost" required
|
||||
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>
|
||||
@@ -32,7 +32,7 @@ templ WebDAVSourceForm() {
|
||||
</div>
|
||||
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
|
||||
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" />
|
||||
placeholder="username" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your WebDAV username
|
||||
@@ -47,7 +47,7 @@ templ WebDAVSourceForm() {
|
||||
</div>
|
||||
<input type="password" id="source_password" name="source_password" x-model="sourcePassword" required
|
||||
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" />
|
||||
placeholder="password" />
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Your WebDAV account password
|
||||
|
||||
@@ -33,22 +33,36 @@ templ ShowToastJS() {
|
||||
textColorClass = 'text-blue-500 dark:text-blue-200';
|
||||
}
|
||||
|
||||
// Set inner HTML with appropriate icon and message
|
||||
toast.innerHTML = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}">
|
||||
${type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>'}
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">${message}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#${toast.id}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
// Create icon div
|
||||
const iconDiv = document.createElement('div');
|
||||
iconDiv.className = `inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}`;
|
||||
iconDiv.innerHTML = type === 'success'
|
||||
? '<i class="fas fa-check"></i>'
|
||||
: type === 'error'
|
||||
? '<i class="fas fa-exclamation-circle"></i>'
|
||||
: '<i class="fas fa-info-circle"></i>';
|
||||
|
||||
// Create message div and set text content safely
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'ml-3 text-sm font-normal';
|
||||
messageDiv.textContent = message; // Use textContent for safety
|
||||
|
||||
// Create close button
|
||||
const closeButton = document.createElement('button'); // Keep this declaration
|
||||
closeButton.type = 'button';
|
||||
closeButton.className = 'ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700';
|
||||
closeButton.setAttribute('data-dismiss-target', `#${toast.id}`);
|
||||
closeButton.setAttribute('aria-label', 'Close');
|
||||
closeButton.innerHTML = `
|
||||
<span class="sr-only">Close</span>
|
||||
<i class="fas fa-times"></i>
|
||||
`;
|
||||
|
||||
// Append elements to the toast
|
||||
toast.appendChild(iconDiv);
|
||||
toast.appendChild(messageDiv);
|
||||
toast.appendChild(closeButton);
|
||||
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
@@ -58,9 +72,8 @@ templ ShowToastJS() {
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
// Add event listener to the close button we created earlier
|
||||
closeButton.addEventListener('click', function() { // Use the existing closeButton variable
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package toast
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func ShowToastJS() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t// Notification system\n\t\tfunction showToast(message, type) {\n\t\t\tconst toastContainer = document.getElementById('toast-container');\n\t\t\tif (!toastContainer) {\n\t\t\t\tconsole.error(\"Toast container not found!\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create toast element\n\t\t\tconst toast = document.createElement('div');\n\t\t\ttoast.id = 'toast-' + type + '-' + Date.now();\n\t\t\ttoast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';\n\t\t\ttoast.role = 'alert';\n\n\t\t\t// Set toast content based on type\n\t\t\tlet iconClass, bgColorClass, textColorClass;\n\n\t\t\tif (type === 'success') {\n\t\t\t\ticonClass = 'text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200';\n\t\t\t\tbgColorClass = 'text-green-500 dark:text-green-200';\n\t\t\t\ttextColorClass = 'text-green-500 dark:text-green-200';\n\t\t\t} else if (type === 'error') {\n\t\t\t\ticonClass = 'text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200';\n\t\t\t\tbgColorClass = 'text-red-500 dark:text-red-200';\n\t\t\t\ttextColorClass = 'text-red-500 dark:text-red-200';\n\t\t\t} else { // Default to info\n\t\t\t\ticonClass = 'text-blue-500 bg-blue-100 dark:bg-blue-800 dark:text-blue-200';\n\t\t\t\tbgColorClass = 'text-blue-500 dark:text-blue-200';\n\t\t\t\ttextColorClass = 'text-blue-500 dark:text-blue-200';\n\t\t\t}\n\n\t\t\t// Set inner HTML with appropriate icon and message\n\t\t\ttoast.innerHTML = `\n\t\t\t\t<div class=\"inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ${iconClass}\">\n\t\t\t\t\t${type === 'success'\n\t\t\t\t\t\t? '<i class=\"fas fa-check\"></i>'\n\t\t\t\t\t\t: type === 'error'\n\t\t\t\t\t\t? '<i class=\"fas fa-exclamation-circle\"></i>'\n\t\t\t\t\t\t: '<i class=\"fas fa-info-circle\"></i>'}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"ml-3 text-sm font-normal\">${message}</div>\n\t\t\t\t<button type=\"button\" class=\"ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700\" data-dismiss-target=\"#${toast.id}\" aria-label=\"Close\">\n\t\t\t\t\t<span class=\"sr-only\">Close</span>\n\t\t\t\t\t<i class=\"fas fa-times\"></i>\n\t\t\t\t</button>\n\t\t\t`;\n\n\t\t\t// Add toast to container\n\t\t\ttoastContainer.appendChild(toast);\n\n\t\t\t// Trigger animation after a small delay\n\t\t\tsetTimeout(() => {\n\t\t\t\ttoast.classList.remove('translate-y-16', 'opacity-0');\n\t\t\t\ttoast.classList.add('translate-y-0', 'opacity-100');\n\t\t\t}, 10);\n\n\t\t\t// Add event listener to close button\n\t\t\tconst closeButton = toast.querySelector('button[data-dismiss-target]');\n\t\t\tcloseButton.addEventListener('click', function() {\n\t\t\t\t// Animate out before removing\n\t\t\t\ttoast.classList.add('opacity-0', 'translate-y-4');\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\ttoast.remove();\n\t\t\t\t}, 300);\n\t\t\t});\n\n\t\t\t// Auto-remove toast after 5 seconds\n\t\t\tsetTimeout(() => {\n\t\t\t\ttoast.classList.add('opacity-0', 'translate-y-4');\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\ttoast.remove();\n\t\t\t\t}, 300);\n\t\t\t}, 5000);\n\t\t}\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,40 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package toast
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Container() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"toast-container\" class=\"fixed top-5 right-5 z-50 flex flex-col gap-2\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
+56
-19
@@ -26,12 +26,22 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
||||
deluser ${USERNAME} > /dev/null 2>&1 || true
|
||||
delgroup ${USERNAME} > /dev/null 2>&1 || true
|
||||
|
||||
# Add group with the specified GID
|
||||
echo "Adding group ${USERNAME} with GID ${PGID}"
|
||||
if ! addgroup -g ${PGID} ${USERNAME}; then
|
||||
echo "⚠️ Failed to add group ${USERNAME} with GID ${PGID}."
|
||||
# Exiting because user creation will likely fail
|
||||
exit 1
|
||||
# Check if a group with the target GID already exists
|
||||
EXISTING_GROUP=$(getent group ${PGID} | cut -d: -f1 || echo "")
|
||||
|
||||
if [ -n "${EXISTING_GROUP}" ]; then
|
||||
echo "Group with GID ${PGID} already exists as '${EXISTING_GROUP}', will use this group"
|
||||
# Set USERNAME_GROUP to the existing group name
|
||||
USERNAME_GROUP="${EXISTING_GROUP}"
|
||||
else
|
||||
# Add group with the specified GID
|
||||
echo "Adding group ${USERNAME} with GID ${PGID}"
|
||||
if ! addgroup -g ${PGID} ${USERNAME}; then
|
||||
echo "⚠️ Failed to add group ${USERNAME} with GID ${PGID}."
|
||||
# Exiting because user creation will likely fail
|
||||
exit 1
|
||||
fi
|
||||
USERNAME_GROUP="${USERNAME}"
|
||||
fi
|
||||
|
||||
# Add user with the specified UID and GID
|
||||
@@ -39,16 +49,16 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
||||
# Use -h /app for home directory (consistent with expectations)
|
||||
# Use -s /bin/sh for shell
|
||||
# Use -D for no password (system user)
|
||||
echo "Adding user ${USERNAME} with UID ${PUID}"
|
||||
if ! adduser -u ${PUID} -G ${USERNAME} -h /app -s /bin/sh -D ${USERNAME}; then
|
||||
echo "⚠️ Failed to add user ${USERNAME} with UID ${PUID} and group ${USERNAME}."
|
||||
echo "Adding user ${USERNAME} with UID ${PUID} and group ${USERNAME_GROUP}"
|
||||
if ! adduser -u ${PUID} -G ${USERNAME_GROUP} -h /app -s /bin/sh -D ${USERNAME}; then
|
||||
echo "⚠️ Failed to add user ${USERNAME} with UID ${PUID} and group ${USERNAME_GROUP}."
|
||||
# Exiting because the application cannot run as the correct user
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the change
|
||||
FINAL_UID=$(id -u ${USERNAME} 2>/dev/null || echo "error")
|
||||
FINAL_GID=$(getent group ${USERNAME} | cut -d: -f3 2>/dev/null || echo "error")
|
||||
FINAL_GID=$(id -g ${USERNAME} 2>/dev/null || echo "error")
|
||||
|
||||
if [ "${FINAL_UID}" = "${PUID}" ] && [ "${FINAL_GID}" = "${PGID}" ]; then
|
||||
echo "✅ Successfully updated UID/GID to ${PUID}:${PGID}"
|
||||
@@ -79,13 +89,40 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
||||
groupdel ${USERNAME} 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Recreate group and user in the correct order
|
||||
echo "Creating group ${USERNAME} with GID ${PGID}"
|
||||
groupadd -g ${PGID} ${USERNAME} 2>/dev/null || groupadd ${USERNAME} 2>/dev/null || true
|
||||
# Check if a group with the target GID already exists
|
||||
EXISTING_GROUP=$(getent group ${PGID} | cut -d: -f1 || echo "")
|
||||
|
||||
echo "Creating user ${USERNAME} with UID ${PUID}"
|
||||
useradd -u ${PUID} -g ${USERNAME} -s /bin/sh ${USERNAME} 2>/dev/null ||
|
||||
useradd -g ${USERNAME} -s /bin/sh ${USERNAME} 2>/dev/null || true
|
||||
if [ -n "${EXISTING_GROUP}" ]; then
|
||||
echo "Group with GID ${PGID} already exists as '${EXISTING_GROUP}', will use this group"
|
||||
# Set USERNAME_GROUP to the existing group name
|
||||
USERNAME_GROUP="${EXISTING_GROUP}"
|
||||
else
|
||||
# Recreate group with the specified GID
|
||||
echo "Creating group ${USERNAME} with GID ${PGID}"
|
||||
groupadd -g ${PGID} ${USERNAME} 2>/dev/null || groupadd ${USERNAME} 2>/dev/null || true
|
||||
USERNAME_GROUP="${USERNAME}"
|
||||
fi
|
||||
|
||||
# Check if a user with the target UID already exists
|
||||
EXISTING_USER=$(getent passwd ${PUID} | cut -d: -f1 || echo "")
|
||||
|
||||
if [ -n "${EXISTING_USER}" ] && [ "${EXISTING_USER}" != "${USERNAME}" ]; then
|
||||
echo "⚠️ Warning: User with UID ${PUID} already exists as '${EXISTING_USER}'. Using a different username may cause issues."
|
||||
fi
|
||||
|
||||
echo "Creating user ${USERNAME} with UID ${PUID} and group ${USERNAME_GROUP}"
|
||||
useradd -u ${PUID} -g ${USERNAME_GROUP} -s /bin/sh ${USERNAME} 2>/dev/null ||
|
||||
useradd -g ${USERNAME_GROUP} -s /bin/sh ${USERNAME} 2>/dev/null || true
|
||||
|
||||
# Verify the change
|
||||
FINAL_UID=$(id -u ${USERNAME} 2>/dev/null || echo "error")
|
||||
FINAL_GID=$(id -g ${USERNAME} 2>/dev/null || echo "error")
|
||||
|
||||
if [ "${FINAL_UID}" = "${PUID}" ] && [ "${FINAL_GID}" = "${PGID}" ]; then
|
||||
echo "✅ Successfully updated UID/GID to ${PUID}:${PGID}"
|
||||
else
|
||||
echo "⚠️ Warning: Verification failed. Target: ${PUID}:${PGID}, Actual: ${FINAL_UID}:${FINAL_GID}"
|
||||
fi
|
||||
} || {
|
||||
echo "⚠️ Warning: Failed to update UID/GID, continuing with built-in user"
|
||||
}
|
||||
@@ -93,17 +130,17 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
||||
|
||||
# Fix ownership of app directories
|
||||
echo "Setting ownership of app directories"
|
||||
chown -R ${USERNAME}:${USERNAME} /app/data /app/backups || echo "⚠️ Warning: Failed to change ownership"
|
||||
chown -R ${USERNAME}:${USERNAME_GROUP:-${USERNAME}} /app/data /app/backups || echo "⚠️ Warning: Failed to change ownership"
|
||||
|
||||
# Ensure .env file exists and has correct permissions
|
||||
if [ -f /app/.env ]; then
|
||||
echo "Found .env file, setting permissions..."
|
||||
chown ${USERNAME}:${USERNAME} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
||||
chown ${USERNAME}:${USERNAME_GROUP:-${USERNAME}} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
||||
chmod 644 /app/.env || echo "⚠️ Warning: Failed to change .env permissions"
|
||||
else
|
||||
echo "No .env file found, creating empty one..."
|
||||
touch /app/.env
|
||||
chown ${USERNAME}:${USERNAME} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
||||
chown ${USERNAME}:${USERNAME_GROUP:-${USERNAME}} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
||||
chmod 644 /app/.env || echo "⚠️ Warning: Failed to change .env permissions"
|
||||
fi
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ require (
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
|
||||
@@ -56,6 +56,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -294,6 +295,12 @@ func handleUpdateConfig(database *db.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate the rclone config file
|
||||
if err := database.GenerateRcloneConfig(&updatedConfig); err != nil {
|
||||
// Log the error but continue anyway as the config was updated in the database
|
||||
log.Printf("Warning: Failed to regenerate rclone config after API update: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, updatedConfig)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-7
@@ -33,6 +33,21 @@ func Initialize(dbPath string) (*DB, error) {
|
||||
return nil, fmt.Errorf("failed to run migrations: %v", err)
|
||||
}
|
||||
|
||||
// Close the database connection after migrations
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get underlying database: %v", err)
|
||||
}
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
return nil, fmt.Errorf("failed to close database after migrations: %v", err)
|
||||
}
|
||||
|
||||
// Reopen the database connection for a clean state
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reconnect to database after migrations: %v", err)
|
||||
}
|
||||
|
||||
return &DB{DB: db}, nil
|
||||
}
|
||||
|
||||
@@ -55,10 +70,3 @@ func (db *DB) Close() error {
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
|
||||
// GetEnabledAuthProviders returns all enabled authentication providers
|
||||
// func (db *DB) GetEnabledAuthProviders(ctx context.Context) ([]AuthProvider, error) {
|
||||
// var providers []AuthProvider
|
||||
// result := db.WithContext(ctx).Where("enabled = ?", true).Find(&providers)
|
||||
// return providers, result.Error
|
||||
// }
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@ import (
|
||||
// table might have been left renamed as _transfer_configs_old.
|
||||
func RecoverTransferConfigsRename() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "011a_recover_transfer_configs_rename",
|
||||
ID: "012a_recover_transfer_configs_rename",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
fmt.Println("Running migration 011a: Checking for transfer_configs rename recovery...")
|
||||
fmt.Println("Running migration 012a: Checking for transfer_configs rename recovery...")
|
||||
|
||||
var oldTableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_transfer_configs_old'").Scan(&oldTableExists)
|
||||
@@ -40,7 +40,7 @@ func RecoverTransferConfigsRename() *gormigrate.Migration {
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// Rollback doesn't make sense for a recovery step.
|
||||
fmt.Println("Rollback for migration 011a_recover_transfer_configs_rename is not applicable.")
|
||||
fmt.Println("Rollback for migration 012a_recover_transfer_configs_rename is not applicable.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RecoverNotificationServicesRename checks for and corrects a specific inconsistent state
|
||||
// left by a potentially failed run of migration 012, where the notification_services
|
||||
// table might have been left renamed as _notification_services_old.
|
||||
func RecoverNotificationServicesRename() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "012b_recover_notification_services_rename",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
fmt.Println("Running migration 012b: Checking for notification_services rename recovery...")
|
||||
|
||||
var oldTableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_notification_services_old'").Scan(&oldTableExists)
|
||||
|
||||
var newTableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='notification_services'").Scan(&newTableExists)
|
||||
|
||||
if oldTableExists > 0 && newTableExists == 0 {
|
||||
fmt.Println("Found _notification_services_old table but not notification_services. Attempting recovery rename...")
|
||||
if err := tx.Exec("ALTER TABLE _notification_services_old RENAME TO notification_services").Error; err != nil {
|
||||
return fmt.Errorf("failed to rename _notification_services_old back to notification_services: %w", err)
|
||||
}
|
||||
fmt.Println("Successfully renamed _notification_services_old to notification_services.")
|
||||
} else if oldTableExists > 0 && newTableExists > 0 {
|
||||
fmt.Println("Warning: Both notification_services and _notification_services_old tables exist. Manual inspection might be needed.")
|
||||
} else {
|
||||
fmt.Println("No recovery needed for notification_services rename.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// Rollback doesn't make sense for a recovery step.
|
||||
fmt.Println("Rollback for migration 012b_recover_notification_services_rename is not applicable.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RecoverAuthProvidersRename checks for and corrects a specific inconsistent state
|
||||
// left by a potentially failed run of migration 012, where the auth_providers
|
||||
// table might have been left renamed as _auth_providers_old.
|
||||
func RecoverAuthProvidersRename() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "012c_recover_auth_providers_rename",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
fmt.Println("Running migration 012c: Checking for auth_providers rename recovery...")
|
||||
|
||||
var oldTableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_auth_providers_old'").Scan(&oldTableExists)
|
||||
|
||||
var newTableExists int
|
||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='auth_providers'").Scan(&newTableExists)
|
||||
|
||||
if oldTableExists > 0 && newTableExists == 0 {
|
||||
fmt.Println("Found _auth_providers_old table but not auth_providers. Attempting recovery rename...")
|
||||
if err := tx.Exec("ALTER TABLE _auth_providers_old RENAME TO auth_providers").Error; err != nil {
|
||||
return fmt.Errorf("failed to rename _auth_providers_old back to auth_providers: %w", err)
|
||||
}
|
||||
fmt.Println("Successfully renamed _auth_providers_old to auth_providers.")
|
||||
} else if oldTableExists > 0 && newTableExists > 0 {
|
||||
fmt.Println("Warning: Both auth_providers and _auth_providers_old tables exist. Manual inspection might be needed.")
|
||||
} else {
|
||||
fmt.Println("No recovery needed for auth_providers rename.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// Rollback doesn't make sense for a recovery step.
|
||||
fmt.Println("Rollback for migration 012c_recover_auth_providers_rename is not applicable.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -11,20 +11,22 @@ var migrations []*gormigrate.Migration
|
||||
func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
// Add all migrations in order
|
||||
migrations = append(migrations,
|
||||
InitialSchema(), // 001
|
||||
UpdateGDriveType(), // 002
|
||||
Add2FA(), // 003
|
||||
AddAuditLogs(), // 004
|
||||
AddDefaultRoles(), // 005
|
||||
AddTimestampsToJobHistories(), // 006
|
||||
AddNotificationServices(), // 007
|
||||
AddUserNotifications(), // 008
|
||||
AddRcloneTables(), // 009
|
||||
AddRcloneCommandToConfig(), // 010
|
||||
AddAuthProviders(), // 011
|
||||
RecoverTransferConfigsRename(), // 011a
|
||||
AlterBooleanDefaults(), // 012
|
||||
CleanupInvalidBooleans(), // 013
|
||||
InitialSchema(), // 001
|
||||
UpdateGDriveType(), // 002
|
||||
Add2FA(), // 003
|
||||
AddAuditLogs(), // 004
|
||||
AddDefaultRoles(), // 005
|
||||
AddTimestampsToJobHistories(), // 006
|
||||
AddNotificationServices(), // 007
|
||||
AddUserNotifications(), // 008
|
||||
AddRcloneTables(), // 009
|
||||
AddRcloneCommandToConfig(), // 010
|
||||
AddAuthProviders(), // 011
|
||||
AlterBooleanDefaults(), // 012
|
||||
RecoverTransferConfigsRename(), // 012a
|
||||
RecoverNotificationServicesRename(), // 012b
|
||||
RecoverAuthProvidersRename(), // 012c
|
||||
CleanupInvalidBooleans(), // 013
|
||||
)
|
||||
|
||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||
|
||||
@@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -90,7 +91,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
sourceName := fmt.Sprintf("source_%d", config.ID)
|
||||
// Generate rclone config using rclone CLI for source
|
||||
switch config.SourceType {
|
||||
case "sftp":
|
||||
case "sftp", "hetzner":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "sftp",
|
||||
"host", config.SourceHost,
|
||||
@@ -129,6 +130,43 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (s3): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "wasabi":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "s3",
|
||||
"provider", "Wasabi",
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.SourceAccessKey,
|
||||
"secret_access_key", config.SourceSecretKey,
|
||||
"region", config.SourceRegion,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
endpoint := config.SourceEndpoint
|
||||
if endpoint == "" {
|
||||
endpoint = "s3.wasabisys.com"
|
||||
}
|
||||
args = append(args, "endpoint", endpoint)
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (wasabi): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "b2":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "b2",
|
||||
"account", config.SourceAccessKey, // B2 Account ID
|
||||
"key", config.SourceSecretKey, // B2 Application Key
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.SourceEndpoint != "" {
|
||||
args = append(args, "endpoint", config.SourceEndpoint)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (b2): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "minio":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "s3",
|
||||
@@ -149,7 +187,49 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (minio): %v\nOutput: %s", err, output)
|
||||
}
|
||||
// ... (Add cases for other source types: b2, smb, ftp, webdav, nextcloud, onedrive, gdrive, gphotos) ...
|
||||
case "webdav", "nextcloud": // Handle both webdav and nextcloud similarly
|
||||
// Construct the WebDAV URL
|
||||
// Parse the provided source URL, assuming it includes the scheme
|
||||
inputURL := config.SourceHost
|
||||
parsedURL, err := url.Parse(inputURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse source URL '%s': %v", inputURL, err)
|
||||
}
|
||||
// Validate that both scheme and host are present
|
||||
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
||||
return fmt.Errorf("invalid source URL '%s': must include scheme (http/https) and host", inputURL)
|
||||
}
|
||||
// Use the scheme and host from the parsed URL
|
||||
webdavURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
|
||||
|
||||
// Determine vendor based on type
|
||||
vendor := "other" // Default vendor
|
||||
if config.SourceType == "nextcloud" {
|
||||
vendor = "nextcloud"
|
||||
|
||||
// Construct the full Nextcloud path using the parsed base URL
|
||||
webdavURL = fmt.Sprintf("%s/remote.php/dav/files/%s/", webdavURL, config.SourceUser)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"config", "create", sourceName, "webdav",
|
||||
"url", webdavURL,
|
||||
"vendor", vendor,
|
||||
"user", config.SourceUser,
|
||||
"pass", config.SourcePassword, // rclone obscures this
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
errorMsg := fmt.Sprintf("failed to create source config (%s): %v", config.SourceType, err)
|
||||
// Check if output contains useful info, especially for auth errors
|
||||
if len(output) > 0 {
|
||||
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
||||
}
|
||||
return fmt.Errorf(errorMsg)
|
||||
}
|
||||
case "local":
|
||||
// For local source, ensure the section exists but might not need specific rclone config create
|
||||
content := fmt.Sprintf("[%s]\ntype = local\n\n", sourceName)
|
||||
@@ -165,7 +245,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||
// Generate rclone config using rclone CLI for destination
|
||||
switch config.DestinationType {
|
||||
case "sftp":
|
||||
case "sftp", "hetzner":
|
||||
args := []string{
|
||||
"config", "create", destName, "sftp",
|
||||
"host", config.DestHost,
|
||||
@@ -204,6 +284,43 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (s3): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "wasabi":
|
||||
args := []string{
|
||||
"config", "create", destName, "s3",
|
||||
"provider", "Wasabi",
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.DestAccessKey,
|
||||
"secret_access_key", config.DestSecretKey,
|
||||
"region", config.DestRegion,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
endpoint := config.DestEndpoint
|
||||
if endpoint == "" {
|
||||
endpoint = "s3.wasabisys.com"
|
||||
}
|
||||
args = append(args, "endpoint", endpoint)
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (wasabi): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "b2":
|
||||
args := []string{
|
||||
"config", "create", destName, "b2",
|
||||
"account", config.DestAccessKey, // B2 Account ID
|
||||
"key", config.DestSecretKey, // B2 Application Key
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.DestEndpoint != "" {
|
||||
args = append(args, "endpoint", config.DestEndpoint)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (b2): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "minio":
|
||||
args := []string{
|
||||
"config", "create", destName, "s3",
|
||||
@@ -224,7 +341,47 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (minio): %v\nOutput: %s", err, output)
|
||||
}
|
||||
// ... (Add cases for other destination types: b2, smb, ftp, webdav, nextcloud, onedrive, gdrive, gphotos) ...
|
||||
case "webdav", "nextcloud": // Combined case for WebDAV and Nextcloud
|
||||
// Parse and reconstruct the WebDAV URL robustly
|
||||
// Parse the provided destination URL, assuming it includes the scheme
|
||||
inputURL := config.DestHost
|
||||
parsedURL, err := url.Parse(inputURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse destination URL '%s': %v", inputURL, err)
|
||||
}
|
||||
// Validate that both scheme and host are present
|
||||
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
||||
return fmt.Errorf("invalid destination URL '%s': must include scheme (http/https) and host", inputURL)
|
||||
}
|
||||
// Use the scheme and host from the parsed URL
|
||||
webdavURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
|
||||
|
||||
// Determine vendor based on type
|
||||
vendor := "other" // Default vendor
|
||||
if config.DestinationType == "nextcloud" {
|
||||
vendor = "nextcloud"
|
||||
|
||||
webdavURL = fmt.Sprintf("%s/remote.php/dav/files/%s/", webdavURL, config.DestUser) // Corrected variable
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"config", "create", destName, "webdav",
|
||||
"url", webdavURL, // Use the parsed and reconstructed URL
|
||||
"vendor", vendor,
|
||||
"user", config.DestUser,
|
||||
"pass", config.DestPassword, // rclone obscures this
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
errorMsg := fmt.Sprintf("failed to create destination config (%s): %v", config.DestinationType, err)
|
||||
if len(output) > 0 {
|
||||
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
||||
}
|
||||
return fmt.Errorf(errorMsg)
|
||||
}
|
||||
case "local":
|
||||
// Append local config section
|
||||
content := fmt.Sprintf("\n[%s]\ntype = local\n", destName)
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/starfleetcptn/gomft/internal/web"
|
||||
)
|
||||
|
||||
var (
|
||||
// Global logger instance
|
||||
stdLogger *Logger
|
||||
|
||||
// Mutex to protect the logger
|
||||
loggerMutex sync.RWMutex
|
||||
|
||||
// Flag to prevent recursive logging
|
||||
isLogging sync.Mutex
|
||||
|
||||
// Log levels
|
||||
LevelDebug = "debug"
|
||||
LevelInfo = "info"
|
||||
LevelWarning = "warn"
|
||||
LevelError = "error"
|
||||
LevelFatal = "fatal"
|
||||
|
||||
// Regex to parse standard log lines (YYYY/MM/DD HH:MM:SS file:line msg)
|
||||
// Adjust if Lmicroseconds is used
|
||||
logLineRegex *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Check log flags to build the correct regex
|
||||
flags := log.Flags()
|
||||
timestampFormat := `\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}`
|
||||
if flags&log.Lmicroseconds != 0 {
|
||||
timestampFormat += `\.\d{6}`
|
||||
}
|
||||
fileFormat := ``
|
||||
if flags&log.Lshortfile != 0 || flags&log.Llongfile != 0 {
|
||||
fileFormat = ` (.+?:\d+): ` // Group 1: file:line
|
||||
}
|
||||
// Regex captures: 1=file:line (optional), 2=message
|
||||
logLineRegex = regexp.MustCompile(fmt.Sprintf(`^%s%s(.*)$`, timestampFormat, fileFormat))
|
||||
}
|
||||
|
||||
// Logger is a custom logger that broadcasts to WebSocket and writes to file
|
||||
type Logger struct {
|
||||
fileWriter io.Writer
|
||||
broadcast bool
|
||||
}
|
||||
|
||||
// Setup initializes the global logger
|
||||
func Setup(logsDir string, broadcast bool) error {
|
||||
// Create logs directory if it doesn't exist
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create or open the log file
|
||||
logFilePath := filepath.Join(logsDir, "scheduler.log")
|
||||
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
|
||||
// Create a multi-writer to log to both stderr and file
|
||||
multiWriter := io.MultiWriter(os.Stderr, logFile)
|
||||
|
||||
// Initialize the logger with mutex protection
|
||||
loggerMutex.Lock()
|
||||
defer loggerMutex.Unlock()
|
||||
|
||||
stdLogger = &Logger{
|
||||
fileWriter: multiWriter,
|
||||
broadcast: broadcast,
|
||||
}
|
||||
|
||||
// Configure the standard log package to use our custom logger
|
||||
log.SetOutput(stdLogger)
|
||||
// Ensure standard flags are set (adjust regex if flags change)
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
|
||||
|
||||
log.Printf("Logger initialized: broadcasting to WebSocket = %v, file = %s", broadcast, logFilePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write implements io.Writer interface for capturing standard log output
|
||||
func (l *Logger) Write(p []byte) (n int, err error) {
|
||||
// Write to the original outputs first
|
||||
n, err = l.fileWriter.Write(p)
|
||||
if err != nil {
|
||||
return n, err // Return error from underlying writer
|
||||
}
|
||||
|
||||
if !l.broadcast {
|
||||
return n, nil // Broadcasting disabled
|
||||
}
|
||||
|
||||
// Use a mutex to prevent recursive logging from BroadcastLog itself
|
||||
if !isLogging.TryLock() {
|
||||
return n, nil // Already processing a log, skip to avoid recursion
|
||||
}
|
||||
defer isLogging.Unlock()
|
||||
|
||||
// Parse the full log line
|
||||
logLine := string(p)
|
||||
level, source, message := parseLogEntry(logLine)
|
||||
|
||||
// *** DEBUG: Print parsed result to stderr ***
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-LOGGER] Parsed: Level='%s', Source='%s', Message='%s'\n", level, source, strings.TrimSpace(message))
|
||||
|
||||
// --- TEMPORARILY DISABLED FILTER ---
|
||||
/*
|
||||
// Don't broadcast logs about WebSocket activity to avoid potential loops
|
||||
if source == "handler" || source == "admin_handlers" || strings.Contains(message, "WebSocket") || strings.Contains(message, "Broadcasting log") || source == "routes" {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-LOGGER] Filtered out log from source '%s'\n", source)
|
||||
return n, nil
|
||||
}
|
||||
*/
|
||||
// --- END TEMPORARILY DISABLED FILTER ---
|
||||
|
||||
// Broadcast to WebSocket clients if handlers are initialized
|
||||
if handlers, ok := web.GetHandlersInstance(); ok && handlers != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-LOGGER] Broadcasting: Level='%s', Source='%s'\n", level, source)
|
||||
handlers.BroadcastLog(level, message, source) // Pass parsed values
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-LOGGER] Skipped broadcast: handlers not ready\n")
|
||||
}
|
||||
|
||||
return n, nil // Return the number of bytes written and no error
|
||||
}
|
||||
|
||||
// parseLogEntry extracts level, source, and message from a standard Go log line
|
||||
func parseLogEntry(logLine string) (level, source, message string) {
|
||||
// Default values
|
||||
level = LevelInfo
|
||||
source = "system"
|
||||
message = strings.TrimSpace(logLine) // Use full line as message by default
|
||||
|
||||
matches := logLineRegex.FindStringSubmatch(logLine)
|
||||
flags := log.Flags()
|
||||
hasFileInfo := flags&log.Lshortfile != 0 || flags&log.Llongfile != 0
|
||||
|
||||
msgIndex := 1 // Index of the message part in regex matches
|
||||
if hasFileInfo {
|
||||
msgIndex = 2
|
||||
}
|
||||
|
||||
if len(matches) > msgIndex {
|
||||
rawMessage := strings.TrimSpace(matches[msgIndex])
|
||||
message = rawMessage // Assign raw message first
|
||||
|
||||
// Extract source from file info if present
|
||||
if hasFileInfo && len(matches) > 1 && matches[1] != "" {
|
||||
fileInfo := matches[1]
|
||||
parts := strings.Split(fileInfo, ":")
|
||||
if len(parts) > 0 {
|
||||
fileName := filepath.Base(parts[0])
|
||||
source = strings.TrimSuffix(fileName, ".go")
|
||||
}
|
||||
} else {
|
||||
// Attempt to infer source if no file info
|
||||
if strings.Contains(rawMessage, "scheduler") {
|
||||
source = "scheduler"
|
||||
} // Add other inferences if needed
|
||||
}
|
||||
|
||||
// Now, parse the level based on prefixes *within* the rawMessage
|
||||
parsedLevel, cleanMessage := parseLevelFromMessage(rawMessage)
|
||||
level = parsedLevel // Update level if prefix found
|
||||
message = cleanMessage // Update message to remove prefix
|
||||
|
||||
} else {
|
||||
// Regex didn't match, try basic prefix check on the whole line (fallback)
|
||||
level, message = parseLevelFromMessage(message) // Use original full message
|
||||
}
|
||||
|
||||
return level, source, message
|
||||
}
|
||||
|
||||
// parseLevelFromMessage checks for level prefixes within a message string
|
||||
func parseLevelFromMessage(msg string) (level string, cleanMsg string) {
|
||||
level = LevelInfo // Default
|
||||
cleanMsg = msg
|
||||
|
||||
// Check common prefixes
|
||||
if strings.HasPrefix(msg, "DEBUG:") {
|
||||
level = LevelDebug
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "DEBUG:"))
|
||||
} else if strings.HasPrefix(msg, "INFO:") {
|
||||
level = LevelInfo
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "INFO:"))
|
||||
} else if strings.HasPrefix(msg, "ERROR:") {
|
||||
level = LevelError
|
||||
cleanMsg = strings.TrimPrefix(msg, "ERROR:")
|
||||
} else if strings.HasPrefix(msg, "WARN:") {
|
||||
level = LevelWarning
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "WARN:"))
|
||||
} else if strings.HasPrefix(msg, "WARNING:") {
|
||||
level = LevelWarning
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "WARNING:"))
|
||||
} else if strings.HasPrefix(msg, "FATAL:") {
|
||||
level = LevelFatal
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "FATAL:"))
|
||||
} else if strings.HasPrefix(msg, "[debug]") {
|
||||
level = LevelDebug
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[debug]"))
|
||||
} else if strings.HasPrefix(msg, "[info]") {
|
||||
level = LevelInfo
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[info]"))
|
||||
} else if strings.HasPrefix(msg, "[warn]") {
|
||||
level = LevelWarning
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[warn]"))
|
||||
} else if strings.HasPrefix(msg, "[warning]") {
|
||||
level = LevelWarning
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[warning]"))
|
||||
} else if strings.HasPrefix(msg, "[error]") {
|
||||
level = LevelError
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[error]"))
|
||||
} else if strings.HasPrefix(msg, "[fatal]") {
|
||||
level = LevelFatal
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[fatal]"))
|
||||
}
|
||||
|
||||
return level, cleanMsg
|
||||
}
|
||||
|
||||
// GetLogger returns the global logger instance
|
||||
func GetLogger() *Logger {
|
||||
loggerMutex.RLock()
|
||||
defer loggerMutex.RUnlock()
|
||||
return stdLogger
|
||||
}
|
||||
|
||||
// Debug logs a debug message
|
||||
func Debug(format string, v ...interface{}) {
|
||||
loggerMutex.RLock()
|
||||
defer loggerMutex.RUnlock()
|
||||
|
||||
if stdLogger == nil {
|
||||
// Fall back to standard logger if not initialized
|
||||
log.Printf("[debug] "+format, v...)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[debug] "+format, v...)
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func Info(format string, v ...interface{}) {
|
||||
loggerMutex.RLock()
|
||||
defer loggerMutex.RUnlock()
|
||||
|
||||
if stdLogger == nil {
|
||||
// Fall back to standard logger if not initialized
|
||||
log.Printf("[info] "+format, v...)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[info] "+format, v...)
|
||||
}
|
||||
|
||||
// Warn logs a warning message
|
||||
func Warn(format string, v ...interface{}) {
|
||||
loggerMutex.RLock()
|
||||
defer loggerMutex.RUnlock()
|
||||
|
||||
if stdLogger == nil {
|
||||
// Fall back to standard logger if not initialized
|
||||
log.Printf("[warn] "+format, v...)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[warn] "+format, v...)
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func Error(format string, v ...interface{}) {
|
||||
loggerMutex.RLock()
|
||||
defer loggerMutex.RUnlock()
|
||||
|
||||
if stdLogger == nil {
|
||||
// Fall back to standard logger if not initialized
|
||||
log.Printf("[error] "+format, v...)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[error] "+format, v...)
|
||||
}
|
||||
|
||||
// Fatal logs a fatal message and exits
|
||||
func Fatal(format string, v ...interface{}) {
|
||||
loggerMutex.RLock()
|
||||
defer loggerMutex.RUnlock()
|
||||
|
||||
if stdLogger == nil {
|
||||
// Fall back to standard logger if not initialized
|
||||
log.Fatalf("[fatal] "+format, v...)
|
||||
return
|
||||
}
|
||||
|
||||
log.Fatalf("[fatal] "+format, v...)
|
||||
}
|
||||
@@ -90,8 +90,20 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
||||
rclonePath = "rclone"
|
||||
}
|
||||
|
||||
primaryProvider := provider
|
||||
|
||||
// Convert hetzner to sftp
|
||||
if provider == "hetzner" {
|
||||
primaryProvider = "sftp"
|
||||
}
|
||||
|
||||
// S3 compatible providers
|
||||
if provider == "minio" || provider == "wasabi" {
|
||||
primaryProvider = "s3"
|
||||
}
|
||||
|
||||
createArgs := []string{
|
||||
"config", "create", remoteName, provider,
|
||||
"config", "create", remoteName, primaryProvider,
|
||||
"--config", tempConfigPath,
|
||||
"--non-interactive",
|
||||
"--log-level", "DEBUG",
|
||||
@@ -105,7 +117,7 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
||||
var createCmd *exec.Cmd
|
||||
|
||||
switch provider {
|
||||
case "sftp":
|
||||
case "sftp", "hetzner":
|
||||
createArgs = append(createArgs, "host", host, "user", user)
|
||||
if port != 0 {
|
||||
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
|
||||
@@ -130,6 +142,20 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
||||
if endpoint != "" {
|
||||
createArgs = append(createArgs, "endpoint", endpoint)
|
||||
}
|
||||
case "wasabi":
|
||||
createArgs = append(createArgs, "provider", "Wasabi", "env_auth", "false")
|
||||
if accessKey != "" {
|
||||
createArgs = append(createArgs, "access_key_id", accessKey)
|
||||
}
|
||||
if secretKey != "" {
|
||||
createArgs = append(createArgs, "secret_access_key", secretKey)
|
||||
}
|
||||
if region != "" {
|
||||
createArgs = append(createArgs, "region", region)
|
||||
}
|
||||
if endpoint != "" {
|
||||
createArgs = append(createArgs, "endpoint", endpoint)
|
||||
}
|
||||
case "minio":
|
||||
createArgs = append(createArgs, "provider", "Minio", "env_auth", "false")
|
||||
if accessKey != "" {
|
||||
@@ -138,6 +164,20 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
||||
if secretKey != "" {
|
||||
createArgs = append(createArgs, "secret_access_key", secretKey)
|
||||
}
|
||||
if region != "" {
|
||||
createArgs = append(createArgs, "region", region)
|
||||
}
|
||||
if endpoint != "" {
|
||||
createArgs = append(createArgs, "endpoint", endpoint)
|
||||
}
|
||||
case "b2":
|
||||
createArgs = append(createArgs, "provider", "B2", "env_auth", "false")
|
||||
if accessKey != "" {
|
||||
createArgs = append(createArgs, "account", accessKey)
|
||||
}
|
||||
if secretKey != "" {
|
||||
createArgs = append(createArgs, "key", secretKey)
|
||||
}
|
||||
if endpoint != "" {
|
||||
createArgs = append(createArgs, "endpoint", endpoint)
|
||||
}
|
||||
@@ -170,12 +210,12 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
||||
createArgs = append(createArgs, "domain", domain)
|
||||
}
|
||||
case "webdav":
|
||||
createArgs = append(createArgs, "url", endpoint, "vendor", "other", "user", user)
|
||||
createArgs = append(createArgs, "url", host, "vendor", "other", "user", user)
|
||||
if pass != "" {
|
||||
createArgs = append(createArgs, "pass", pass)
|
||||
}
|
||||
case "nextcloud":
|
||||
createArgs = append(createArgs, "url", endpoint, "vendor", "nextcloud", "user", user)
|
||||
createArgs = append(createArgs, "url", host, "vendor", "nextcloud", "user", user)
|
||||
if pass != "" {
|
||||
createArgs = append(createArgs, "pass", pass)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ const (
|
||||
LogLevelDebug
|
||||
)
|
||||
|
||||
// BroadcastFunc is a function type that can be used to broadcast logs
|
||||
type BroadcastFunc func(level, message, source string)
|
||||
|
||||
// String returns the string representation of a log level
|
||||
func (l LogLevel) String() string {
|
||||
switch l {
|
||||
@@ -54,31 +57,70 @@ func ParseLogLevel(level string) LogLevel {
|
||||
|
||||
// Logger handles log output to file and console
|
||||
type Logger struct {
|
||||
Info *log.Logger
|
||||
Error *log.Logger
|
||||
Debug *log.Logger
|
||||
file *lumberjack.Logger
|
||||
logLevel LogLevel
|
||||
Info *log.Logger
|
||||
Error *log.Logger
|
||||
Debug *log.Logger
|
||||
file *lumberjack.Logger
|
||||
logLevel LogLevel
|
||||
useBroadcast bool
|
||||
broadcastFn BroadcastFunc
|
||||
}
|
||||
|
||||
// SetBroadcastFunc sets the function to use for broadcasting logs
|
||||
func (l *Logger) SetBroadcastFunc(fn BroadcastFunc) {
|
||||
l.broadcastFn = fn
|
||||
l.useBroadcast = fn != nil
|
||||
|
||||
// Log the setting of the broadcast function to help with troubleshooting
|
||||
if fn != nil {
|
||||
fmt.Fprintf(os.Stderr, "[SCHEDULER-LOGGER] Broadcast function set successfully, logs will be streamed to WebSocket clients\n")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[SCHEDULER-LOGGER] Broadcast function cleared or set to nil\n")
|
||||
}
|
||||
}
|
||||
|
||||
// LogInfo logs an info message if the log level allows it
|
||||
func (l *Logger) LogInfo(format string, v ...interface{}) {
|
||||
if l.logLevel >= LogLevelInfo {
|
||||
l.Info.Printf(format, v...)
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
l.Info.Println(msg)
|
||||
|
||||
// If broadcasting is enabled, call the broadcast function
|
||||
if l.useBroadcast && l.broadcastFn != nil {
|
||||
// Add debug output
|
||||
fmt.Fprintf(os.Stderr, "[SCHEDULER-LOGGER-BROADCAST] INFO: %s\n", msg)
|
||||
l.broadcastFn("info", msg, "scheduler")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogError logs an error message if the log level allows it
|
||||
func (l *Logger) LogError(format string, v ...interface{}) {
|
||||
if l.logLevel >= LogLevelError {
|
||||
l.Error.Printf(format, v...)
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
l.Error.Println(msg)
|
||||
|
||||
// If broadcasting is enabled, call the broadcast function
|
||||
if l.useBroadcast && l.broadcastFn != nil {
|
||||
// Add debug output
|
||||
fmt.Fprintf(os.Stderr, "[SCHEDULER-LOGGER-BROADCAST] ERROR: %s\n", msg)
|
||||
l.broadcastFn("error", msg, "scheduler")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogDebug logs a debug message if the log level allows it
|
||||
func (l *Logger) LogDebug(format string, v ...interface{}) {
|
||||
if l.logLevel >= LogLevelDebug {
|
||||
l.Debug.Printf(format, v...)
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
l.Debug.Println(msg)
|
||||
|
||||
// If broadcasting is enabled, call the broadcast function
|
||||
if l.useBroadcast && l.broadcastFn != nil {
|
||||
// Add debug output
|
||||
fmt.Fprintf(os.Stderr, "[SCHEDULER-LOGGER-BROADCAST] DEBUG: %s\n", msg)
|
||||
l.broadcastFn("debug", msg, "scheduler")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +175,12 @@ func NewLogger() *Logger {
|
||||
logLevel = ParseLogLevel(envLogLevel)
|
||||
}
|
||||
|
||||
// Check if we should enable WebSocket broadcasting
|
||||
useBroadcast := true
|
||||
if envBroadcast := os.Getenv("LOG_BROADCAST"); envBroadcast == "false" {
|
||||
useBroadcast = false
|
||||
}
|
||||
|
||||
// Setup log rotation
|
||||
logFile := &lumberjack.Logger{
|
||||
Filename: filepath.Join(logsDir, "scheduler.log"),
|
||||
@@ -147,17 +195,19 @@ func NewLogger() *Logger {
|
||||
|
||||
// Create loggers with different prefixes
|
||||
logger := &Logger{
|
||||
Info: log.New(consoleAndFile, "INFO: ", log.Ldate|log.Ltime),
|
||||
Error: log.New(consoleAndFile, "ERROR: ", log.Ldate|log.Ltime),
|
||||
Debug: log.New(consoleAndFile, "DEBUG: ", log.Ldate|log.Ltime),
|
||||
file: logFile,
|
||||
logLevel: logLevel,
|
||||
Info: log.New(consoleAndFile, "INFO: ", log.Ldate|log.Ltime),
|
||||
Error: log.New(consoleAndFile, "ERROR: ", log.Ldate|log.Ltime),
|
||||
Debug: log.New(consoleAndFile, "DEBUG: ", log.Ldate|log.Ltime),
|
||||
file: logFile,
|
||||
logLevel: logLevel,
|
||||
useBroadcast: useBroadcast,
|
||||
broadcastFn: nil, // Will be set later
|
||||
}
|
||||
|
||||
// Log rotation settings and log level
|
||||
if logLevel >= LogLevelInfo {
|
||||
logger.Info.Printf("Log rotation configured: file=%s, maxSize=%dMB, maxBackups=%d, maxAge=%d days, compress=%v, logLevel=%s",
|
||||
filepath.Join(logsDir, "scheduler.log"), maxSize, maxBackups, maxAge, compress, logLevel.String())
|
||||
logger.Info.Printf("Log rotation configured: file=%s, maxSize=%dMB, maxBackups=%d, maxAge=%d days, compress=%v, logLevel=%s, useBroadcast=%v",
|
||||
filepath.Join(logsDir, "scheduler.log"), maxSize, maxBackups, maxAge, compress, logLevel.String(), useBroadcast)
|
||||
}
|
||||
|
||||
if logLevel >= LogLevelDebug {
|
||||
|
||||
@@ -915,8 +915,8 @@ func (n *Notifier) sendNtfyNotification(service *db.NotificationService, job *db
|
||||
title := replaceVariables(titleTemplate, variables) // Use package-level helper
|
||||
message := replaceVariables(messageTemplate, variables) // Use package-level helper
|
||||
|
||||
// Create the URL for the notification
|
||||
ntfyURL := fmt.Sprintf("%s/%s", strings.TrimRight(server, "/"), topic)
|
||||
// Create the URL for the notification - do not append topic
|
||||
ntfyURL := strings.TrimRight(server, "/")
|
||||
|
||||
// Create the notification data
|
||||
ntfyData := map[string]interface{}{
|
||||
@@ -926,7 +926,7 @@ func (n *Notifier) sendNtfyNotification(service *db.NotificationService, job *db
|
||||
"priority": priority,
|
||||
}
|
||||
|
||||
// Add username and password if provided
|
||||
// Get username and password if provided
|
||||
username := service.Config["username"]
|
||||
password := service.Config["password"]
|
||||
|
||||
@@ -936,6 +936,9 @@ func (n *Notifier) sendNtfyNotification(service *db.NotificationService, job *db
|
||||
return fmt.Errorf("failed to marshal ntfy notification data: %v", err)
|
||||
}
|
||||
|
||||
// Debug log the payload
|
||||
n.logger.LogDebug("ntfy URL: %s, payload: %s", ntfyURL, string(jsonData))
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("POST", ntfyURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/internal/config"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
@@ -14,13 +16,27 @@ type Handler struct {
|
||||
handlers *handlers.Handlers
|
||||
}
|
||||
|
||||
// Global handlers instance for access from other packages
|
||||
var globalHandlersInstance *handlers.Handlers
|
||||
|
||||
// GetHandlersInstance returns the global handlers instance and a boolean indicating if it's initialized
|
||||
func GetHandlersInstance() (*handlers.Handlers, bool) {
|
||||
return globalHandlersInstance, globalHandlersInstance != nil
|
||||
}
|
||||
|
||||
// NewHandler creates a new Handler instance that delegates to the handlers package
|
||||
func NewHandler(database *db.DB, scheduler *scheduler.Scheduler, jwtSecret string, dbPath string, backupDir string, cfg *config.Config) (*Handler, error) {
|
||||
// Create email service instance
|
||||
emailService := email.NewService(cfg)
|
||||
|
||||
// Use logs directory from config
|
||||
logsDir := filepath.Join(cfg.DataDir, "logs")
|
||||
|
||||
// Create handlers instance
|
||||
handlersInstance := handlers.NewHandlers(database, scheduler, jwtSecret, dbPath, backupDir, "./logs", emailService)
|
||||
handlersInstance := handlers.NewHandlers(database, scheduler, jwtSecret, dbPath, backupDir, logsDir, emailService)
|
||||
|
||||
// Store the handlers instance globally
|
||||
globalHandlersInstance = handlersInstance
|
||||
|
||||
return &Handler{
|
||||
handlers: handlersInstance,
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
@@ -1278,3 +1285,440 @@ func (h *Handlers) HandleDeleteUser(c *gin.Context) {
|
||||
// Always use the partial for HTMX delete requests
|
||||
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
||||
}
|
||||
|
||||
// HandleLogViewer renders the log viewer page
|
||||
func (h *Handlers) HandleLogViewer(c *gin.Context) {
|
||||
ctx := components.CreateTemplateContext(c)
|
||||
|
||||
// Create logs data for initial page load
|
||||
logFilePath := filepath.Join(h.LogsDir, "scheduler.log")
|
||||
|
||||
// Log the full path for debugging
|
||||
log.Printf("Log viewer initialized with log file path: %s", logFilePath)
|
||||
|
||||
data := components.LogViewerData{
|
||||
Logs: []components.LogEntry{},
|
||||
CurrentFilter: "",
|
||||
LogFilePath: logFilePath,
|
||||
}
|
||||
|
||||
// Render the log viewer component
|
||||
components.AdminLogs(ctx, data).Render(ctx, c.Writer)
|
||||
}
|
||||
|
||||
// HandleLogStream handles WebSocket connections for real-time log streaming
|
||||
func (h *Handlers) HandleLogStream(c *gin.Context) {
|
||||
// Configure upgrader
|
||||
upgrader := websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Allow all origins for now
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] New WebSocket connection request from %s\n", c.ClientIP())
|
||||
|
||||
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Failed to upgrade WebSocket for %s: %v\n", c.ClientIP(), err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] WebSocket connection upgraded for %s\n", c.ClientIP())
|
||||
|
||||
// Set ping handler to respond with pong
|
||||
ws.SetPingHandler(func(data string) error {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Received ping from %s, responding with pong\n", ws.RemoteAddr())
|
||||
return ws.WriteControl(websocket.PongMessage, []byte{}, time.Now().Add(5*time.Second))
|
||||
})
|
||||
|
||||
// Ensure connection is closed eventually
|
||||
defer func() {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Closing WebSocket connection for %s\n", ws.RemoteAddr())
|
||||
ws.Close()
|
||||
}()
|
||||
|
||||
// Register the new client and create its mutex
|
||||
WebSocketClientsMutex.Lock()
|
||||
WebSocketClients[ws] = true
|
||||
WebSocketClientWriteMutexes[ws] = &sync.Mutex{}
|
||||
numClients := len(WebSocketClients)
|
||||
WebSocketClientsMutex.Unlock()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Registered client %s. Total clients: %d\n", ws.RemoteAddr(), numClients)
|
||||
|
||||
// De-register the client when the handler exits
|
||||
defer func() {
|
||||
WebSocketClientsMutex.Lock()
|
||||
delete(WebSocketClients, ws)
|
||||
delete(WebSocketClientWriteMutexes, ws)
|
||||
remainingClients := len(WebSocketClients)
|
||||
WebSocketClientsMutex.Unlock()
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] De-registered client %s. Remaining clients: %d\n", ws.RemoteAddr(), remainingClients)
|
||||
}()
|
||||
|
||||
// Send recent logs immediately after connection
|
||||
h.sendRecentLogs(ws)
|
||||
|
||||
// Start a goroutine to send pings periodically to keep the connection alive
|
||||
stopPinger := make(chan struct{})
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Failed to send ping to client %s: %v\n", ws.RemoteAddr(), err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sent ping to client %s\n", ws.RemoteAddr())
|
||||
case <-stopPinger:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep the connection alive by reading messages (and discarding them)
|
||||
// This also detects when the client closes the connection.
|
||||
for {
|
||||
messageType, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] WebSocket closed unexpectedly for %s: %v\n", ws.RemoteAddr(), err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] WebSocket closed normally for %s.\n", ws.RemoteAddr())
|
||||
}
|
||||
break // Exit loop on Read error
|
||||
}
|
||||
|
||||
// Handle client messages (like ping)
|
||||
if messageType == websocket.TextMessage && len(message) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Received message from client %s: %s\n", ws.RemoteAddr(), message)
|
||||
|
||||
// Try to parse as JSON and check for ping
|
||||
var msgData map[string]interface{}
|
||||
if err := json.Unmarshal(message, &msgData); err == nil {
|
||||
if msgType, ok := msgData["type"].(string); ok && msgType == "ping" {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Received ping from client %s, responding with pong\n", ws.RemoteAddr())
|
||||
|
||||
// Send a pong response
|
||||
pongResp := map[string]interface{}{
|
||||
"type": "pong",
|
||||
"time": time.Now().Unix(),
|
||||
}
|
||||
|
||||
WebSocketClientsMutex.Lock()
|
||||
mutex, exists := WebSocketClientWriteMutexes[ws]
|
||||
WebSocketClientsMutex.Unlock()
|
||||
|
||||
if exists {
|
||||
mutex.Lock()
|
||||
err := ws.WriteJSON(pongResp)
|
||||
mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error sending pong to client %s: %v\n", ws.RemoteAddr(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the ping goroutine
|
||||
close(stopPinger)
|
||||
}
|
||||
|
||||
// sendRecentLogs sends recent log entries to a new WebSocket client
|
||||
func (h *Handlers) sendRecentLogs(ws *websocket.Conn) {
|
||||
// Get the mutex for this client *first*
|
||||
WebSocketClientsMutex.Lock()
|
||||
mutex, exists := WebSocketClientWriteMutexes[ws]
|
||||
if !exists {
|
||||
// This shouldn't happen if HandleLogStream is correct, but handle defensively
|
||||
WebSocketClientsMutex.Unlock()
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Mutex not found for client %v during sendRecentLogs. Aborting recent logs send.\n", ws.RemoteAddr())
|
||||
return
|
||||
}
|
||||
WebSocketClientsMutex.Unlock()
|
||||
|
||||
// Construct the path to the log file
|
||||
logFilePath := filepath.Join(h.LogsDir, "scheduler.log")
|
||||
|
||||
// Check if the log file exists
|
||||
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Log file not found at %s for sendRecentLogs. Sending example logs.\n", logFilePath)
|
||||
h.sendExampleLogs(ws) // Send examples if main log file isn't there
|
||||
return
|
||||
}
|
||||
|
||||
// Open the log file
|
||||
file, err := os.Open(logFilePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error opening log file %s: %v. Sending example logs.\n", logFilePath, err)
|
||||
h.sendExampleLogs(ws)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the last 20 lines
|
||||
lines, err := readLastLines(file, 20)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error reading log file %s: %v. Sending example logs.\n", logFilePath, err)
|
||||
h.sendExampleLogs(ws)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Read %d lines from %s for client %v.\n", len(lines), logFilePath, ws.RemoteAddr())
|
||||
|
||||
// Parse and send each line as a log entry, protected by the client's mutex
|
||||
for _, line := range lines {
|
||||
level, source, message := parseLogLine(line)
|
||||
timestamp := extractTimestamp(line)
|
||||
|
||||
logEntry := components.LogEntry{
|
||||
Timestamp: timestamp,
|
||||
Level: level,
|
||||
Message: message,
|
||||
Source: source,
|
||||
}
|
||||
|
||||
// Use the specific client's mutex
|
||||
// fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sending recent log %d/%d to client %v\n", i+1, len(lines), ws.RemoteAddr())
|
||||
mutex.Lock()
|
||||
err := ws.WriteJSON(logEntry)
|
||||
mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
// fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error sending recent log %d to client %v: %v. Stopping recent logs send.\n", i+1, ws.RemoteAddr(), err)
|
||||
// Don't try to remove the client here, let the main read loop handle it
|
||||
break // Stop sending recent logs on first error
|
||||
}
|
||||
}
|
||||
// fmt.Fprintf(os.Stderr, "[DEBUG-WS] Finished sending %d recent logs to client %v.\n", len(lines), ws.RemoteAddr())
|
||||
}
|
||||
|
||||
// readLastLines reads the last n lines from a file
|
||||
func readLastLines(file *os.File, n int) ([]string, error) {
|
||||
// Implement a simpler version that reads the whole file and keeps the last n lines
|
||||
scanner := bufio.NewScanner(file)
|
||||
var lines []string
|
||||
|
||||
// Read all lines
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the last n lines (or all if less than n)
|
||||
if len(lines) <= n {
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
return lines[len(lines)-n:], nil
|
||||
}
|
||||
|
||||
// parseLogLine extracts level, source, and message from a log line
|
||||
// This is specific to the format found in the log file being read,
|
||||
// NOT the format generated by the standard Go logger directly.
|
||||
func parseLogLine(line string) (level, source, message string) {
|
||||
// Default values
|
||||
level = "info"
|
||||
source = "system"
|
||||
originalLine := line // Keep original for prefix check
|
||||
|
||||
// Check for level prefixes first
|
||||
foundPrefix := false
|
||||
if strings.HasPrefix(originalLine, "DEBUG:") { // Check original line for prefix
|
||||
level = "debug"
|
||||
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "DEBUG:"))
|
||||
foundPrefix = true
|
||||
} else if strings.HasPrefix(originalLine, "INFO:") {
|
||||
level = "info"
|
||||
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "INFO:"))
|
||||
foundPrefix = true
|
||||
} else if strings.HasPrefix(originalLine, "ERROR:") {
|
||||
level = "error"
|
||||
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "ERROR:"))
|
||||
foundPrefix = true
|
||||
} else if strings.HasPrefix(originalLine, "WARN:") {
|
||||
level = "warn"
|
||||
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "WARN:"))
|
||||
foundPrefix = true
|
||||
} else if strings.HasPrefix(originalLine, "WARNING:") {
|
||||
level = "warn"
|
||||
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "WARNING:"))
|
||||
foundPrefix = true
|
||||
} else if strings.HasPrefix(originalLine, "FATAL:") {
|
||||
level = "fatal"
|
||||
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "FATAL:"))
|
||||
foundPrefix = true
|
||||
}
|
||||
|
||||
// Now parse the rest (timestamp + message) using the potentially modified 'line'
|
||||
parts := strings.SplitN(line, " ", 3)
|
||||
if len(parts) >= 3 {
|
||||
message = parts[2] // The rest is the message
|
||||
|
||||
// Try to extract source *only if no level prefix was found initially*
|
||||
// Assumes standard log format prefixes message with file:line
|
||||
if !foundPrefix {
|
||||
if fileStart := strings.Index(message, " "); fileStart > 0 {
|
||||
filePath := message[:fileStart]
|
||||
if strings.Contains(filePath, ":") {
|
||||
filePathParts := strings.Split(filePath, "/")
|
||||
if len(filePathParts) > 0 {
|
||||
fileNameWithLine := filePathParts[len(filePathParts)-1]
|
||||
fileName := strings.Split(fileNameWithLine, ":")[0]
|
||||
source = strings.TrimSuffix(fileName, ".go")
|
||||
}
|
||||
}
|
||||
// Update message to remove the file info
|
||||
message = message[fileStart+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix was found, attempt level detection from message content (e.g., [info])
|
||||
if !foundPrefix {
|
||||
parsedLevel, cleanMessage := parseLevelFromMessageContent(message)
|
||||
level = parsedLevel
|
||||
message = cleanMessage
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback if split doesn't work as expected, use the (potentially prefix-stripped) line
|
||||
message = line
|
||||
}
|
||||
|
||||
return level, source, message
|
||||
}
|
||||
|
||||
// parseLevelFromMessageContent checks for bracketed level indicators
|
||||
func parseLevelFromMessageContent(msg string) (string, string) {
|
||||
level := "info" // Default
|
||||
cleanMsg := msg
|
||||
|
||||
if strings.HasPrefix(msg, "[debug]") {
|
||||
level = "debug"
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[debug]"))
|
||||
} else if strings.HasPrefix(msg, "[info]") {
|
||||
level = "info"
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[info]"))
|
||||
} else if strings.HasPrefix(msg, "[warn]") {
|
||||
level = "warn"
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[warn]"))
|
||||
} else if strings.HasPrefix(msg, "[warning]") {
|
||||
level = "warn"
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[warning]"))
|
||||
} else if strings.HasPrefix(msg, "[error]") {
|
||||
level = "error"
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[error]"))
|
||||
} else if strings.HasPrefix(msg, "[fatal]") {
|
||||
level = "fatal"
|
||||
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[fatal]"))
|
||||
}
|
||||
// Optional: Add inference based on keywords like the logger does
|
||||
// else if strings.Contains(strings.ToLower(msg), "error") { level = "error" } ...
|
||||
return level, cleanMsg
|
||||
}
|
||||
|
||||
// extractTimestamp extracts the timestamp from a log line, handling potential prefixes
|
||||
func extractTimestamp(line string) time.Time {
|
||||
now := time.Now() // Default
|
||||
originalLine := line
|
||||
|
||||
// Remove known level prefixes for timestamp parsing
|
||||
prefixes := []string{"DEBUG:", "INFO:", "ERROR:", "WARN:", "WARNING:", "FATAL:"}
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
line = strings.TrimSpace(strings.TrimPrefix(line, prefix))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract timestamp parts (date and time)
|
||||
parts := strings.SplitN(line, " ", 3)
|
||||
if len(parts) >= 2 {
|
||||
dateStr := parts[0]
|
||||
timeStr := parts[1]
|
||||
timestampStr := dateStr + " " + timeStr
|
||||
|
||||
// List of timestamp formats to try
|
||||
formats := []string{
|
||||
"2006/01/02 15:04:05", // Standard Go log with slashes
|
||||
"2006/01/02 15:04:05.999", // With milliseconds
|
||||
"2006/01/02 15:04:05.999999", // With microseconds
|
||||
"2006-01-02 15:04:05", // Standard Go log with dashes
|
||||
"2006-01-02 15:04:05.999", // With milliseconds
|
||||
"2006-01-02 15:04:05.999999", // With microseconds
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
timestamp, err := time.Parse(format, timestampStr)
|
||||
if err == nil {
|
||||
return timestamp // Successfully parsed
|
||||
}
|
||||
}
|
||||
// If all formats failed, log the original attempt
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-TIMESTAMP] Failed to parse timestamp from '%s' (derived from line: %s)\n", timestampStr, originalLine)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-TIMESTAMP] Could not split timestamp parts from line: %s\n", originalLine)
|
||||
}
|
||||
|
||||
return now // Return current time if parsing failed
|
||||
}
|
||||
|
||||
// sendExampleLogs sends example log entries for demonstration
|
||||
func (h *Handlers) sendExampleLogs(ws *websocket.Conn) {
|
||||
// Get the mutex for this client *first*
|
||||
WebSocketClientsMutex.Lock()
|
||||
mutex, exists := WebSocketClientWriteMutexes[ws]
|
||||
if !exists {
|
||||
WebSocketClientsMutex.Unlock()
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Mutex not found for client %v during sendExampleLogs. Aborting example logs send.\n", ws.RemoteAddr())
|
||||
return
|
||||
}
|
||||
WebSocketClientsMutex.Unlock()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sending example logs to client %v\n", ws.RemoteAddr())
|
||||
|
||||
// Example log entries for demonstration
|
||||
exampleLogs := []components.LogEntry{
|
||||
{
|
||||
Timestamp: time.Now().UTC().Add(-time.Minute * 5),
|
||||
Level: "info",
|
||||
Message: "Application started successfully",
|
||||
Source: "main",
|
||||
},
|
||||
{
|
||||
Timestamp: time.Now().UTC().Add(-time.Minute * 3),
|
||||
Level: "debug",
|
||||
Message: "Connected to database",
|
||||
Source: "database",
|
||||
},
|
||||
{
|
||||
Timestamp: time.Now().UTC().Add(-time.Minute * 2),
|
||||
Level: "warn",
|
||||
Message: "High memory usage detected: 85%",
|
||||
Source: "monitor",
|
||||
},
|
||||
}
|
||||
|
||||
for i, logEntry := range exampleLogs {
|
||||
// Use the specific client's mutex
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sending example log %d/%d to client %v\n", i+1, len(exampleLogs), ws.RemoteAddr())
|
||||
mutex.Lock()
|
||||
err := ws.WriteJSON(logEntry)
|
||||
mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error sending example log %d to client %v: %v. Stopping example logs send.\n", i+1, ws.RemoteAddr(), err)
|
||||
break // Stop sending example logs on first error
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Finished sending example logs to client %v.\n", ws.RemoteAddr())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -142,6 +143,12 @@ func (h *Handlers) HandleAPIUpdateConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate the rclone config file
|
||||
if err := h.DB.GenerateRcloneConfig(&config); err != nil {
|
||||
log.Printf("Warning: Failed to regenerate rclone config after API update: %v", err)
|
||||
// Continue anyway, as the config was updated in the database
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"config": config})
|
||||
}
|
||||
|
||||
|
||||
@@ -278,11 +278,20 @@ func (h *Handlers) HandleLoginPage(c *gin.Context) {
|
||||
// Check for message query param (used for password expired, etc.)
|
||||
message := c.Query("message")
|
||||
|
||||
// Check if external providers exist
|
||||
var providerCount int64
|
||||
if err := h.DB.Model(&db.AuthProvider{}).Where("enabled = ?", true).Count(&providerCount).Error; err != nil {
|
||||
log.Printf("Error counting auth providers: %v", err)
|
||||
// Assume no providers if there's an error, or handle differently
|
||||
providerCount = 0
|
||||
}
|
||||
hasExternalProviders := providerCount > 0
|
||||
|
||||
// User is not logged in, show login page
|
||||
if message != "" {
|
||||
components.Login(ctx, message).Render(c.Request.Context(), c.Writer)
|
||||
components.Login(ctx, message, hasExternalProviders).Render(c.Request.Context(), c.Writer)
|
||||
} else {
|
||||
components.Login(ctx, "").Render(c.Request.Context(), c.Writer)
|
||||
components.Login(ctx, "", hasExternalProviders).Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,10 +300,18 @@ func (h *Handlers) HandleLogin(c *gin.Context) {
|
||||
email := c.PostForm("email")
|
||||
password := c.PostForm("password")
|
||||
|
||||
// Check if external providers exist
|
||||
var providerCount int64
|
||||
if err := h.DB.Model(&db.AuthProvider{}).Where("enabled = ?", true).Count(&providerCount).Error; err != nil {
|
||||
log.Printf("Error counting auth providers: %v", err)
|
||||
providerCount = 0
|
||||
}
|
||||
hasExternalProviders := providerCount > 0
|
||||
|
||||
// Get user by email
|
||||
var user db.User
|
||||
if err := h.DB.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
components.Login(components.CreateTemplateContext(c), "Invalid credentials").Render(c, c.Writer)
|
||||
components.Login(components.CreateTemplateContext(c), "Invalid credentials", hasExternalProviders).Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -308,7 +325,7 @@ func (h *Handlers) HandleLogin(c *gin.Context) {
|
||||
h.DB.Save(&user)
|
||||
} else {
|
||||
// Account is still locked
|
||||
components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.").Render(c, c.Writer)
|
||||
components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.", hasExternalProviders).Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -325,12 +342,12 @@ func (h *Handlers) HandleLogin(c *gin.Context) {
|
||||
lockoutTime := time.Now().Add(policy.LockoutDuration)
|
||||
user.LockoutUntil = &lockoutTime
|
||||
h.DB.Save(&user)
|
||||
components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.").Render(c, c.Writer)
|
||||
components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.", hasExternalProviders).Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
h.DB.Save(&user)
|
||||
components.Login(components.CreateTemplateContext(c), "Invalid credentials").Render(c, c.Writer)
|
||||
components.Login(components.CreateTemplateContext(c), "Invalid credentials", hasExternalProviders).Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -368,7 +385,7 @@ func (h *Handlers) HandleLogin(c *gin.Context) {
|
||||
}
|
||||
token, err := h.GenerateJWT(user.ID, user.Email, isAdmin)
|
||||
if err != nil {
|
||||
components.Login(components.CreateTemplateContext(c), "Authentication error").Render(c, c.Writer)
|
||||
components.Login(components.CreateTemplateContext(c), "Authentication error", hasExternalProviders).Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ func (h *Handlers) HandleCreateConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Generate rclone config file
|
||||
|
||||
if err := h.DB.GenerateRcloneConfig(&config); err != nil {
|
||||
log.Printf("Warning: Failed to generate rclone config: %v", err)
|
||||
// Continue anyway, as the config was created in the database
|
||||
@@ -462,6 +463,14 @@ func (h *Handlers) HandleUpdateConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate the rclone config file
|
||||
if err := h.DB.GenerateRcloneConfig(&config); err != nil {
|
||||
log.Printf("Warning: Failed to regenerate rclone config after update: %v", err)
|
||||
// Continue anyway, as the config was updated in the database
|
||||
} else {
|
||||
log.Printf("Regenerated rclone config for config ID %d after update", config.ID)
|
||||
}
|
||||
|
||||
// Redirect to the configs page
|
||||
c.Redirect(http.StatusSeeOther, "/configs")
|
||||
}
|
||||
|
||||
@@ -101,16 +101,14 @@ func (h *Handlers) HandleGDriveAuth(c *gin.Context) {
|
||||
}
|
||||
|
||||
if config.DestClientID != "" && config.DestClientSecret == "" {
|
||||
// If user provided just client ID but no secret, try to find the secret in the config
|
||||
_, existingClientSecret := h.DB.GetGDriveCredentialsFromConfig(config)
|
||||
|
||||
if existingClientSecret != "" {
|
||||
// Use the secret from the existing config with the provided client ID
|
||||
clientSecret = existingClientSecret
|
||||
// If user provided just client ID but no secret, use environment variable or default
|
||||
envSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
if envSecret != "" {
|
||||
// Use the secret from environment variable
|
||||
clientSecret = envSecret
|
||||
} else {
|
||||
// If we still can't find a matching secret, show an error
|
||||
RenderErrorPage(c, "Missing client secret", "You provided a custom client ID but no client secret. Both are required for Google authentication.")
|
||||
return
|
||||
// Use default rclone client secret
|
||||
clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,16 +237,14 @@ func (h *Handlers) HandleGDriveAuthCallback(c *gin.Context) {
|
||||
}
|
||||
|
||||
if config.DestClientID != "" && config.DestClientSecret == "" {
|
||||
// If user provided just client ID but no secret, try to find the secret in the config
|
||||
_, existingClientSecret := h.DB.GetGDriveCredentialsFromConfig(config)
|
||||
|
||||
if existingClientSecret != "" {
|
||||
// Use the secret from the existing config with the provided client ID
|
||||
clientSecret = existingClientSecret
|
||||
// If user provided just client ID but no secret, use environment variable or default
|
||||
envSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
if envSecret != "" {
|
||||
// Use the secret from environment variable
|
||||
clientSecret = envSecret
|
||||
} else {
|
||||
// If we still can't find a matching secret, show an error
|
||||
RenderErrorPage(c, "Missing client secret", "You provided a custom client ID but no client secret. Both are required for Google authentication.")
|
||||
return
|
||||
// Use default rclone client secret
|
||||
clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"github.com/starfleetcptn/gomft/internal/email"
|
||||
"github.com/starfleetcptn/gomft/internal/scheduler"
|
||||
)
|
||||
|
||||
// WebSocketClients maintains the set of active WebSocket clients
|
||||
var WebSocketClients = make(map[*websocket.Conn]bool)
|
||||
|
||||
// WebSocketClientsMutex protects the WebSocketClients map
|
||||
var WebSocketClientsMutex = &sync.Mutex{}
|
||||
|
||||
// WebSocketClientWriteMutexes maintains individual write mutexes for each client
|
||||
var WebSocketClientWriteMutexes = make(map[*websocket.Conn]*sync.Mutex)
|
||||
|
||||
// LogChannel is used to send log entries to all WebSocket clients
|
||||
var LogChannel = make(chan components.LogEntry, 512)
|
||||
|
||||
// Handlers contains all the dependencies needed by the handlers
|
||||
type Handlers struct {
|
||||
DB *db.DB
|
||||
@@ -22,7 +39,7 @@ type Handlers struct {
|
||||
|
||||
// NewHandlers creates a new Handlers instance
|
||||
func NewHandlers(database *db.DB, scheduler scheduler.SchedulerInterface, jwtSecret string, dbPath string, backupDir string, logsDir string, emailService *email.Service) *Handlers {
|
||||
return &Handlers{
|
||||
h := &Handlers{
|
||||
DB: database,
|
||||
Scheduler: scheduler,
|
||||
JWTSecret: jwtSecret,
|
||||
@@ -32,4 +49,112 @@ func NewHandlers(database *db.DB, scheduler scheduler.SchedulerInterface, jwtSec
|
||||
LogsDir: logsDir,
|
||||
Email: emailService,
|
||||
}
|
||||
|
||||
// Start the WebSocket log broadcaster
|
||||
StartLogBroadcaster()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// StartLogBroadcaster starts a goroutine that broadcasts logs to all connected WebSocket clients
|
||||
func StartLogBroadcaster() {
|
||||
go func() {
|
||||
fmt.Fprintln(os.Stderr, "[DEBUG-BROADCASTER] Broadcaster goroutine started.")
|
||||
for {
|
||||
logEntry := <-LogChannel // Wait for a log entry
|
||||
|
||||
WebSocketClientsMutex.Lock()
|
||||
clientsToSend := make(map[*websocket.Conn]*sync.Mutex)
|
||||
for client, mutex := range WebSocketClientWriteMutexes {
|
||||
if _, exists := WebSocketClients[client]; exists {
|
||||
clientsToSend[client] = mutex
|
||||
}
|
||||
}
|
||||
WebSocketClientsMutex.Unlock()
|
||||
|
||||
if len(clientsToSend) == 0 {
|
||||
continue // Skip if no clients
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTER] Received log. Broadcasting to %d clients. Level='%s', Src='%s'\n",
|
||||
len(clientsToSend), logEntry.Level, logEntry.Source)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for client, mutex := range clientsToSend {
|
||||
wg.Add(1)
|
||||
go func(c *websocket.Conn, m *sync.Mutex, entry components.LogEntry) {
|
||||
defer wg.Done()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTER] Attempting send to client %v\n", c.RemoteAddr())
|
||||
|
||||
// Lock only for this specific client's write
|
||||
m.Lock()
|
||||
// Set a deadline for the write operation
|
||||
deadline := time.Now().Add(5 * time.Second) // 5-second deadline
|
||||
err := c.SetWriteDeadline(deadline)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTER] Error setting write deadline for client %v: %v\n", c.RemoteAddr(), err)
|
||||
// Don't unlock yet, proceed to cleanup
|
||||
} else {
|
||||
err = c.WriteJSON(entry)
|
||||
}
|
||||
m.Unlock() // Unlock after write attempt (or deadline error)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTER] Error writing to client %v: %v. Initiating removal.\n", c.RemoteAddr(), err)
|
||||
WebSocketClientsMutex.Lock()
|
||||
if _, stillExists := WebSocketClients[c]; stillExists {
|
||||
delete(WebSocketClients, c)
|
||||
delete(WebSocketClientWriteMutexes, c)
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTER] Removed client %v from maps.\n", c.RemoteAddr())
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTER] Client %v already removed by another process.\n", c.RemoteAddr())
|
||||
}
|
||||
WebSocketClientsMutex.Unlock()
|
||||
c.Close() // Close the connection outside the lock
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTER] Successfully sent to client %v\n", c.RemoteAddr())
|
||||
}
|
||||
}(client, mutex, logEntry)
|
||||
}
|
||||
wg.Wait() // Wait for all sends in this batch to complete or fail
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// BroadcastLog sends a log entry to all connected WebSocket clients
|
||||
func (h *Handlers) BroadcastLog(level, message, source string) {
|
||||
// NOTE: Level prefix parsing is now handled in logger.go/parseLogEntry
|
||||
|
||||
// Create log entry with UTC timestamp for consistency
|
||||
logEntry := components.LogEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: level,
|
||||
Message: message,
|
||||
Source: source,
|
||||
}
|
||||
|
||||
// Get the current number of clients (avoid logging in case of recursive issues)
|
||||
numClients := 0
|
||||
WebSocketClientsMutex.Lock()
|
||||
numClients = len(WebSocketClients)
|
||||
WebSocketClientsMutex.Unlock()
|
||||
|
||||
// Only attempt to write to channel if there are clients
|
||||
if numClients > 0 {
|
||||
// *** DEBUG: Print channel send attempt ***
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTLOG] Attempting to send to LogChannel: Level='%s', Source='%s'\n", level, source)
|
||||
|
||||
// Try to send the log entry to the channel with a timeout
|
||||
select {
|
||||
case LogChannel <- logEntry:
|
||||
// Successfully sent
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTLOG] Successfully sent to LogChannel.\n")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// Channel is full or blocked, log and continue
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTLOG] Log channel timeout, discarding log entry: %s\n", message)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG-BROADCASTLOG] No clients connected, skipping send to LogChannel.\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,9 +132,11 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
{
|
||||
adminRoles.GET("", h.HandleRoles)
|
||||
adminRoles.GET("/new", h.HandleNewRole)
|
||||
adminRoles.GET("/:id", h.HandleEditRole)
|
||||
adminRoles.GET("/:id/edit", h.HandleRoles)
|
||||
adminRoles.POST("", h.HandleCreateRole)
|
||||
adminRoles.PUT("/:id", h.HandleEditRole)
|
||||
adminRoles.POST("/:id", h.HandleUpdateRole)
|
||||
adminRoles.PUT("/:id", h.HandleUpdateRole)
|
||||
adminRoles.DELETE("/:id", h.HandleDeleteRole)
|
||||
}
|
||||
|
||||
@@ -146,6 +148,14 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
auditGroup.GET("/export", h.PermissionMiddleware("audit.export"), h.HandleExportAuditLogs)
|
||||
}
|
||||
|
||||
// Log viewer routes
|
||||
logsGroup := admin.Group("/logs")
|
||||
logsGroup.Use(h.PermissionMiddleware("logs.view"))
|
||||
{
|
||||
logsGroup.GET("", h.HandleLogViewer)
|
||||
logsGroup.GET("/ws", h.HandleLogStream)
|
||||
}
|
||||
|
||||
// System settings routes
|
||||
settingsGroup := admin.Group("/settings")
|
||||
settingsGroup.Use(h.PermissionMiddleware("system.settings"))
|
||||
|
||||
@@ -162,59 +162,90 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
|
||||
config["priority"] = c.PostForm("ntfy_priority")
|
||||
config["username"] = c.PostForm("ntfy_username")
|
||||
config["password"] = c.PostForm("ntfy_password")
|
||||
config["title_template"] = c.PostForm("ntfy_title_template")
|
||||
config["message_template"] = c.PostForm("ntfy_message_template")
|
||||
|
||||
// Event triggers are handled above
|
||||
config["title"] = c.PostForm("ntfy_title")
|
||||
|
||||
// Validate required fields
|
||||
if config["server"] == "" || config["topic"] == "" {
|
||||
h.handleNotificationsWithError(c, "Ntfy Server and Topic are required.")
|
||||
if config["topic"] == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Ntfy Topic is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create new notification service
|
||||
service := db.NotificationService{
|
||||
Name: name,
|
||||
Type: serviceType,
|
||||
// IsEnabled will be set using the helper method below
|
||||
Config: config,
|
||||
Description: description,
|
||||
EventTriggers: eventTriggers,
|
||||
CreatedBy: c.GetUint("userID"),
|
||||
// Set default server if not provided
|
||||
if config["server"] == "" {
|
||||
config["server"] = "https://ntfy.sh"
|
||||
}
|
||||
service.SetIsEnabled(isEnabled) // Use helper method
|
||||
|
||||
// Save to database
|
||||
if err := h.DB.Create(&service).Error; err != nil {
|
||||
log.Printf("Error creating notification service: %v", err)
|
||||
h.handleNotificationsWithError(c, "Failed to create notification service: "+err.Error())
|
||||
// Create the URL for the notification - no need to include topic in the URL
|
||||
ntfyURL := strings.TrimRight(config["server"], "/")
|
||||
|
||||
// Create the notification data
|
||||
ntfyData := map[string]interface{}{
|
||||
"topic": config["topic"],
|
||||
"title": "GoMFT Test Notification",
|
||||
"message": "This is a test notification from GoMFT",
|
||||
}
|
||||
|
||||
// Add priority if provided
|
||||
if config["priority"] != "" {
|
||||
priority, err := strconv.Atoi(config["priority"])
|
||||
if err == nil {
|
||||
ntfyData["priority"] = priority
|
||||
} else {
|
||||
ntfyData["priority"] = config["priority"]
|
||||
}
|
||||
}
|
||||
|
||||
// Add title if provided and not empty
|
||||
if config["title"] != "" {
|
||||
ntfyData["title"] = config["title"]
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
jsonData, err := json.Marshal(ntfyData)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to prepare test notification: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create audit log
|
||||
auditDetails := map[string]interface{}{
|
||||
"name": service.Name,
|
||||
"type": service.Type,
|
||||
"is_enabled": service.GetIsEnabled(), // Use getter
|
||||
"description": service.Description,
|
||||
"event_triggers": eventTriggers,
|
||||
// Create request
|
||||
req, err := http.NewRequest("POST", ntfyURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to create request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
auditLog := db.AuditLog{
|
||||
Action: "create",
|
||||
EntityType: "notification_service",
|
||||
EntityID: service.ID,
|
||||
UserID: c.GetUint("userID"),
|
||||
Details: auditDetails,
|
||||
// Set the Content-Type header
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add authentication if provided
|
||||
if config["username"] != "" && config["password"] != "" {
|
||||
req.SetBasicAuth(config["username"], config["password"])
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&auditLog).Error; err != nil {
|
||||
log.Printf("Error creating audit log: %v", err)
|
||||
}
|
||||
// Debug info
|
||||
fmt.Printf("ntfy URL: %s\n", ntfyURL)
|
||||
fmt.Printf("ntfy payload: %s\n", string(jsonData))
|
||||
|
||||
// Redirect back to notifications page with success message
|
||||
h.handleNotificationsWithSuccess(c, "Ntfy notification service created successfully.")
|
||||
// Send the request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to send test notification: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response body for error details if needed
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
fmt.Printf("ntfy response status: %s\n", resp.Status)
|
||||
fmt.Printf("ntfy response body: %s\n", string(respBody))
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Test notification sent successfully"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Test notification returned error: " + resp.Status + " - " + string(respBody)})
|
||||
}
|
||||
return
|
||||
|
||||
case "gotify":
|
||||
@@ -636,8 +667,8 @@ func (h *Handlers) HandleTestNotification(c *gin.Context) {
|
||||
config["server"] = "https://ntfy.sh"
|
||||
}
|
||||
|
||||
// Create the URL for the notification
|
||||
ntfyURL := fmt.Sprintf("%s/%s", strings.TrimRight(config["server"], "/"), config["topic"])
|
||||
// Create the URL for the notification - no need to include topic in the URL
|
||||
ntfyURL := strings.TrimRight(config["server"], "/")
|
||||
|
||||
// Create the notification data
|
||||
ntfyData := map[string]interface{}{
|
||||
@@ -648,32 +679,64 @@ func (h *Handlers) HandleTestNotification(c *gin.Context) {
|
||||
|
||||
// Add priority if provided
|
||||
if config["priority"] != "" {
|
||||
ntfyData["priority"] = config["priority"]
|
||||
priority, err := strconv.Atoi(config["priority"])
|
||||
if err == nil {
|
||||
ntfyData["priority"] = priority
|
||||
} else {
|
||||
ntfyData["priority"] = config["priority"]
|
||||
}
|
||||
}
|
||||
|
||||
// Add title if provided
|
||||
// Add title if provided and not empty
|
||||
if config["title"] != "" {
|
||||
ntfyData["title"] = config["title"]
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
jsonData, err := json.Marshal(ntfyData)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to prepare test notification: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Send the notification
|
||||
resp, err := http.Post(ntfyURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
// Create request
|
||||
req, err := http.NewRequest("POST", ntfyURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to create request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Set the Content-Type header
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add authentication if provided
|
||||
if config["username"] != "" && config["password"] != "" {
|
||||
req.SetBasicAuth(config["username"], config["password"])
|
||||
}
|
||||
|
||||
// Debug info
|
||||
fmt.Printf("ntfy URL: %s\n", ntfyURL)
|
||||
fmt.Printf("ntfy payload: %s\n", string(jsonData))
|
||||
|
||||
// Send the request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to send test notification: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response body for error details if needed
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
fmt.Printf("ntfy response status: %s\n", resp.Status)
|
||||
fmt.Printf("ntfy response body: %s\n", string(respBody))
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Test notification sent successfully"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Test notification returned non-success status code: " + resp.Status})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Test notification returned error: " + resp.Status + " - " + string(respBody)})
|
||||
}
|
||||
return
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/config"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"github.com/starfleetcptn/gomft/internal/logging"
|
||||
"github.com/starfleetcptn/gomft/internal/scheduler"
|
||||
"github.com/starfleetcptn/gomft/internal/web"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -65,14 +66,27 @@ func main() {
|
||||
// Set Gin to release mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.Printf("Starting GoMFT server version %s...", components.AppVersion)
|
||||
|
||||
// Initialize configuration
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
fmt.Printf("Failed to load configuration: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Ensure logs directory exists
|
||||
logsDir := filepath.Join(cfg.DataDir, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
fmt.Printf("Failed to create logs directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize logger with file output and WebSocket broadcasting
|
||||
if err := logging.Setup(logsDir, true); err != nil {
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Printf("Starting GoMFT server version %s...", components.AppVersion)
|
||||
log.Printf("Configuration loaded successfully")
|
||||
|
||||
// Ensure required directories exist
|
||||
@@ -206,6 +220,14 @@ func main() {
|
||||
webHandler.InitializeRoutes(router)
|
||||
log.Printf("Web handlers initialized successfully")
|
||||
|
||||
// Connect scheduler logger to WebSocket broadcast system
|
||||
if handlers, ok := web.GetHandlersInstance(); ok && handlers != nil {
|
||||
schedLogger.SetBroadcastFunc(handlers.BroadcastLog)
|
||||
log.Printf("Scheduler logger connected to WebSocket broadcast system")
|
||||
} else {
|
||||
log.Printf("Warning: Could not connect scheduler logger to WebSocket broadcast system - handlers not ready")
|
||||
}
|
||||
|
||||
// Initialize API routes
|
||||
// Commenting out the API routes initialization to avoid route conflicts
|
||||
// api.InitializeRoutes(router, database, scheduler, cfg.JWTSecret)
|
||||
|
||||
+1
-1
@@ -449,7 +449,7 @@ function enhanceMobileForms() {
|
||||
|
||||
|
||||
// Listen for custom 'showToast' event triggered by HX-Trigger
|
||||
document.body.addEventListener('showToast', function(event) {
|
||||
document.addEventListener('showToast', function(event) { // Changed from document.body
|
||||
// Debug logs removed
|
||||
if (event.detail && event.detail.message && event.detail.type) {
|
||||
// Call the globally defined showToast function from toast_js.templ
|
||||
|
||||
Reference in New Issue
Block a user