Skip to content

Commit 989a625

Browse files
JohanDevlclaude
andcommitted
fix: implement real export functionality and improve UI feedback
- Replace placeholder export handlers with actual async export execution - Add proper command-line argument building for export commands - Implement auto-refresh system for export list updates - Fix export buttons stuck in processing state with reset functionality - Add visual feedback with auto-refresh indicator - Improve error handling with detailed logging and output capture - Remove WebSocket dependency for simpler, more reliable progress tracking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d55fa33 commit 989a625

File tree

2 files changed

+166
-22
lines changed

2 files changed

+166
-22
lines changed

pkg/web/handlers/exports.go

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"html/template"
77
"net/http"
88
"os"
9+
"os/exec"
910
"path/filepath"
1011
"strconv"
1112
"strings"
@@ -160,12 +161,14 @@ func (h *ExportsHandler) handleStartExport(w http.ResponseWriter, r *http.Reques
160161
"client_ip": r.RemoteAddr,
161162
})
162163

163-
// In a real implementation, this would trigger the actual export
164-
// For now, we'll just log it and return success
164+
// Start export in background
165+
exportID := fmt.Sprintf("export_%d", time.Now().Unix())
166+
go h.runExportAsync(exportID, exportType, historyMode)
167+
165168
h.writeJSONResponse(w, ExportAPIResponse{
166169
Success: true,
167170
Data: map[string]interface{}{
168-
"export_id": fmt.Sprintf("export_%d", time.Now().Unix()),
171+
"export_id": exportID,
169172
"type": exportType,
170173
"status": "started",
171174
},
@@ -572,6 +575,64 @@ func (h *ExportsHandler) writeJSONResponse(w http.ResponseWriter, response Expor
572575
}
573576
}
574577

578+
// runExportAsync executes an export command asynchronously
579+
func (h *ExportsHandler) runExportAsync(exportID, exportType, historyMode string) {
580+
h.logger.Info("web.export_async_started", map[string]interface{}{
581+
"export_id": exportID,
582+
"export_type": exportType,
583+
"history_mode": historyMode,
584+
})
585+
586+
// Find the current executable path
587+
execPath, err := os.Executable()
588+
if err != nil {
589+
h.logger.Error("web.export_async_failed", map[string]interface{}{
590+
"export_id": exportID,
591+
"error": "Could not find executable path: " + err.Error(),
592+
})
593+
return
594+
}
595+
596+
// Build command arguments
597+
args := []string{
598+
"--run",
599+
"--export", exportType,
600+
"--mode", "complete",
601+
}
602+
603+
// Add history mode for watched exports
604+
if exportType == "watched" && historyMode != "" {
605+
args = append(args, "--history-mode", historyMode)
606+
}
607+
608+
h.logger.Info("web.export_async_command", map[string]interface{}{
609+
"export_id": exportID,
610+
"command": execPath,
611+
"args": strings.Join(args, " "),
612+
})
613+
614+
// Execute the command
615+
cmd := exec.Command(execPath, args...)
616+
cmd.Env = os.Environ() // Inherit environment variables
617+
618+
// Capture both stdout and stderr for better debugging
619+
output, err := cmd.CombinedOutput()
620+
621+
if err != nil {
622+
h.logger.Error("web.export_async_failed", map[string]interface{}{
623+
"export_id": exportID,
624+
"error": err.Error(),
625+
"output": string(output),
626+
"command": execPath + " " + strings.Join(args, " "),
627+
})
628+
} else {
629+
h.logger.Info("web.export_async_completed", map[string]interface{}{
630+
"export_id": exportID,
631+
"output": string(output),
632+
})
633+
}
634+
}
635+
575636
// DownloadHandler handles file downloads
576637
type DownloadHandler struct {
577638
exportsDir string

web/templates/exports.html

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ <h2>📋 Export History</h2>
141141
{{else}}
142142
Total: {{if .Exports}}{{len .Exports}}{{else}}0{{end}} exports
143143
{{end}}
144+
<span id="auto-refresh-indicator" style="display: none; color: #28a745; margin-left: 10px;">
145+
🔄 Auto-refreshing...
146+
</span>
144147
</div>
145148
</div>
146149

@@ -315,25 +318,15 @@ <h3>No exports found</h3>
315318

316319
function startExport(type, options = {}) {
317320
// Show progress section
318-
document.getElementById("export-progress").style.display = "block";
319-
320-
// Start WebSocket connection for real-time updates
321-
if (window.WebSocket) {
322-
exportSocket = new WebSocket(`ws://${window.location.host}/ws/export`);
323-
324-
exportSocket.onmessage = function (event) {
325-
const data = JSON.parse(event.data);
326-
updateProgress(data);
327-
};
328-
329-
exportSocket.onerror = function (error) {
330-
console.error("WebSocket error:", error);
331-
};
332-
333-
exportSocket.onclose = function () {
334-
console.log("WebSocket connection closed");
335-
};
336-
}
321+
const progressSection = document.getElementById("export-progress");
322+
const progressText = document.getElementById("progress-text");
323+
const progressFill = document.getElementById("progress-fill");
324+
const progressPercent = document.getElementById("progress-percent");
325+
326+
progressSection.style.display = "block";
327+
progressText.textContent = "Starting export...";
328+
progressFill.style.width = "0%";
329+
progressPercent.textContent = "0%";
337330

338331
// Trigger export
339332
const params = new URLSearchParams({ type, ...options });
@@ -343,11 +336,25 @@ <h3>No exports found</h3>
343336
if (!data.success) {
344337
showAlert("error", data.error || "Export failed");
345338
hideProgress();
339+
return;
346340
}
341+
342+
// Show export started message
343+
progressText.textContent = "Export started in background...";
344+
progressFill.style.width = "100%";
345+
progressPercent.textContent = "100%";
346+
347+
// Hide progress after 3 seconds and reset button
348+
setTimeout(() => {
349+
hideProgress();
350+
showAlert("success", `Export ${type} started successfully! Auto-refreshing...`);
351+
resetExportButtons();
352+
}, 3000);
347353
})
348354
.catch((error) => {
349355
showAlert("error", "Failed to start export: " + error.message);
350356
hideProgress();
357+
resetExportButtons();
351358
});
352359
}
353360

@@ -659,6 +666,79 @@ <h4>${typeLabel}</h4>
659666
});
660667
}
661668

669+
// Reset export buttons to initial state
670+
function resetExportButtons() {
671+
document.querySelectorAll(".export-btn").forEach((btn) => {
672+
btn.disabled = false;
673+
// Restore original text based on button type
674+
const type = btn.dataset.type;
675+
switch(type) {
676+
case 'watched':
677+
btn.textContent = 'Export Watched';
678+
break;
679+
case 'collection':
680+
btn.textContent = 'Export Collection';
681+
break;
682+
case 'shows':
683+
btn.textContent = 'Export Shows';
684+
break;
685+
case 'ratings':
686+
btn.textContent = 'Export Ratings';
687+
break;
688+
case 'watchlist':
689+
btn.textContent = 'Export Watchlist';
690+
break;
691+
case 'all':
692+
btn.textContent = 'Export All';
693+
break;
694+
default:
695+
btn.textContent = 'Export';
696+
}
697+
});
698+
}
699+
700+
// Auto-refresh functionality for exports
701+
let autoRefreshInterval = null;
702+
703+
function startAutoRefresh() {
704+
// Stop any existing auto-refresh
705+
if (autoRefreshInterval) {
706+
clearInterval(autoRefreshInterval);
707+
}
708+
709+
// Show auto-refresh indicator
710+
const indicator = document.getElementById('auto-refresh-indicator');
711+
if (indicator) {
712+
indicator.style.display = 'inline';
713+
}
714+
715+
// Start auto-refresh every 5 seconds for 30 seconds (6 attempts)
716+
let attempts = 0;
717+
autoRefreshInterval = setInterval(() => {
718+
attempts++;
719+
if (attempts > 6) {
720+
stopAutoRefresh();
721+
return;
722+
}
723+
724+
// Refresh the exports list
725+
loadPage(currentPage, currentPageSize);
726+
}, 5000);
727+
}
728+
729+
function stopAutoRefresh() {
730+
if (autoRefreshInterval) {
731+
clearInterval(autoRefreshInterval);
732+
autoRefreshInterval = null;
733+
}
734+
735+
// Hide auto-refresh indicator
736+
const indicator = document.getElementById('auto-refresh-indicator');
737+
if (indicator) {
738+
indicator.style.display = 'none';
739+
}
740+
}
741+
662742
// Export button handlers
663743
document.addEventListener("DOMContentLoaded", function () {
664744
document.querySelectorAll(".export-btn").forEach((btn) => {
@@ -674,6 +754,9 @@ <h4>${typeLabel}</h4>
674754
}
675755

676756
startExport(type, options);
757+
758+
// Start auto-refresh to catch the new export when it appears
759+
startAutoRefresh();
677760
});
678761
});
679762

0 commit comments

Comments
 (0)