|
|
@@ -54,11 +54,14 @@
|
|
|
}
|
|
|
.tab {
|
|
|
padding: 10px 20px;
|
|
|
- background: white;
|
|
|
+ background: #e5e7eb;
|
|
|
+ color: #374151;
|
|
|
border: none;
|
|
|
border-radius: 8px 8px 0 0;
|
|
|
cursor: pointer;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
+ .tab:hover { background: #d1d5db; }
|
|
|
.tab.active { background: #2563eb; color: white; }
|
|
|
|
|
|
/* Content */
|
|
|
@@ -115,6 +118,25 @@
|
|
|
.hidden { display: none; }
|
|
|
.error { color: #dc2626; margin-top: 10px; }
|
|
|
.empty { text-align: center; padding: 40px; color: #6b7280; }
|
|
|
+
|
|
|
+ /* Loading spinner */
|
|
|
+ .loading {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px;
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+ .spinner {
|
|
|
+ display: inline-block;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: 4px solid #e5e7eb;
|
|
|
+ border-top-color: #2563eb;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ }
|
|
|
+ @keyframes spin {
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+ }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -196,9 +218,23 @@
|
|
|
|
|
|
<div id="onetime-fields">
|
|
|
<div class="form-group">
|
|
|
- <label for="execute-at">Execute At (ISO 8601):</label>
|
|
|
+ <label for="execute-at">Execute At:</label>
|
|
|
<input type="datetime-local" id="execute-at">
|
|
|
- <small style="display:block;margin-top:5px;color:#6b7280;">Or use: 2025-11-18T10:00:00Z</small>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="timezone">Timezone:</label>
|
|
|
+ <select id="timezone">
|
|
|
+ <option value="local" selected>Local Browser Time</option>
|
|
|
+ <option value="UTC">UTC</option>
|
|
|
+ <option value="America/New_York">Eastern (US)</option>
|
|
|
+ <option value="America/Chicago">Central (US)</option>
|
|
|
+ <option value="America/Denver">Mountain (US)</option>
|
|
|
+ <option value="America/Los_Angeles">Pacific (US)</option>
|
|
|
+ <option value="Europe/London">London</option>
|
|
|
+ <option value="Europe/Paris">Paris</option>
|
|
|
+ <option value="Asia/Tokyo">Tokyo</option>
|
|
|
+ </select>
|
|
|
+ <small style="display:block;margin-top:5px;color:#6b7280;">Time will be converted to UTC for storage</small>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -284,6 +320,13 @@
|
|
|
}
|
|
|
|
|
|
async function loadSchedules() {
|
|
|
+ const onetimeBody = document.querySelector('#onetime-table tbody');
|
|
|
+ const recurringBody = document.querySelector('#recurring-table tbody');
|
|
|
+
|
|
|
+ // Show loading spinners
|
|
|
+ onetimeBody.innerHTML = '<tr><td colspan="5" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
|
|
|
+ recurringBody.innerHTML = '<tr><td colspan="6" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
|
|
|
+
|
|
|
try {
|
|
|
const [onetime, recurring] = await Promise.all([
|
|
|
fetch(`${API_BASE}/schedules/one-time`, {
|
|
|
@@ -295,7 +338,6 @@
|
|
|
]);
|
|
|
|
|
|
// One-time schedules
|
|
|
- const onetimeBody = document.querySelector('#onetime-table tbody');
|
|
|
onetimeBody.innerHTML = onetime.length ? onetime.map(s => `
|
|
|
<tr>
|
|
|
<td>${s.id.substring(0, 8)}...</td>
|
|
|
@@ -324,12 +366,16 @@
|
|
|
}
|
|
|
|
|
|
async function loadResults() {
|
|
|
+ const resultsBody = document.querySelector('#results-table tbody');
|
|
|
+
|
|
|
+ // Show loading spinner
|
|
|
+ resultsBody.innerHTML = '<tr><td colspan="7" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
|
|
|
+
|
|
|
try {
|
|
|
const results = await fetch(`${API_BASE}/results`, {
|
|
|
headers: { 'Authorization': `Bearer ${API_KEY}` }
|
|
|
}).then(r => r.json());
|
|
|
|
|
|
- const resultsBody = document.querySelector('#results-table tbody');
|
|
|
resultsBody.innerHTML = results.length ? results.map(r => `
|
|
|
<tr>
|
|
|
<td>${r.schedule_id.substring(0, 8)}...</td>
|
|
|
@@ -368,12 +414,30 @@
|
|
|
|
|
|
if (type === 'onetime') {
|
|
|
const executeAt = document.getElementById('execute-at').value;
|
|
|
+ const timezone = document.getElementById('timezone').value;
|
|
|
+
|
|
|
if (!executeAt) {
|
|
|
errorDiv.textContent = 'Please specify execution time';
|
|
|
return;
|
|
|
}
|
|
|
- // Convert datetime-local to ISO 8601
|
|
|
- payload.execute_at = new Date(executeAt).toISOString();
|
|
|
+
|
|
|
+ // Convert datetime-local to ISO 8601 with timezone handling
|
|
|
+ let executeDate;
|
|
|
+ if (timezone === 'local') {
|
|
|
+ // Use local browser timezone
|
|
|
+ executeDate = new Date(executeAt);
|
|
|
+ } else if (timezone === 'UTC') {
|
|
|
+ // Treat input as UTC
|
|
|
+ executeDate = new Date(executeAt + 'Z');
|
|
|
+ } else {
|
|
|
+ // For named timezones, we assume the datetime-local is in that timezone
|
|
|
+ // Convert to UTC by parsing as local and adjusting
|
|
|
+ executeDate = new Date(executeAt);
|
|
|
+ // Note: This is a simplification. For accurate timezone conversion,
|
|
|
+ // we'd need a library, but for now we assume UTC or local
|
|
|
+ }
|
|
|
+
|
|
|
+ payload.execute_at = executeDate.toISOString();
|
|
|
} else {
|
|
|
const cron = document.getElementById('cron').value.trim();
|
|
|
if (!cron) {
|