Add upload progress bar with SSE extraction status
build-and-push / test (push) Successful in 40s
build-and-push / build-and-push (push) Successful in 1m8s

ISO uploads now show a progress bar during file transfer (via XHR
upload.onprogress) and real-time extraction status (via SSE events
through the existing Hub). Falls back to plain form POST if JS is
disabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 19:16:19 -04:00
parent 4774600040
commit 443a3db9e1
6 changed files with 200 additions and 22 deletions
+10 -2
View File
@@ -160,7 +160,8 @@ func imageUploadForm(errMsg string) string {
return layout("Upload Image", fmt.Sprintf(`
<h2>Upload Boot Image</h2>
%s
<form method="POST" action="/images/upload" enctype="multipart/form-data" class="form">
<form id="upload-form" method="POST" action="/images/upload" enctype="multipart/form-data" class="form">
<input type="hidden" name="upload_id" id="upload-id" value="">
<label>Name<input type="text" name="name" placeholder="proxmox-8.2" required pattern="[a-z0-9][a-z0-9.\-]*"></label>
<label>Version<input type="text" name="version" placeholder="8.2-1" required></label>
<label>Kind
@@ -169,9 +170,16 @@ func imageUploadForm(errMsg string) string {
</select>
</label>
<label>ISO File<input type="file" name="iso" accept=".iso" required></label>
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">Upload may take several minutes for large ISOs. The kernel and initrd will be extracted automatically.</p>
<button type="submit" class="btn">Upload &amp; Extract</button>
</form>
<div id="upload-progress" style="display:none" class="upload-progress">
<h3 id="progress-title">Uploading ISO...</h3>
<div class="progress-bar-track">
<div class="progress-bar-fill" id="progress-fill"></div>
</div>
<div class="progress-text" id="progress-text">Preparing upload...</div>
<div class="progress-detail" id="progress-detail"></div>
</div>
`, errHTML))
}
+38 -8
View File
@@ -167,34 +167,64 @@ func (u *UI) NewImageForm(w http.ResponseWriter, r *http.Request) {
}
func (u *UI) UploadImage(w http.ResponseWriter, r *http.Request) {
isXHR := r.Header.Get("X-Requested-With") == "XMLHttpRequest"
if err := r.ParseMultipartForm(0); err != nil {
renderHTML(w, imageUploadForm("Invalid form submission"))
if isXHR {
writeJSON(w, http.StatusBadRequest, map[string]any{"ok": false, "error": "Invalid form submission"})
} else {
renderHTML(w, imageUploadForm("Invalid form submission"))
}
return
}
name := strings.TrimSpace(r.FormValue("name"))
version := strings.TrimSpace(r.FormValue("version"))
kind := strings.TrimSpace(r.FormValue("kind"))
uploadID := strings.TrimSpace(r.FormValue("upload_id"))
file, _, err := r.FormFile("iso")
if err != nil {
renderHTML(w, imageUploadForm("ISO file is required"))
if isXHR {
writeJSON(w, http.StatusBadRequest, map[string]any{"ok": false, "error": "ISO file is required"})
} else {
renderHTML(w, imageUploadForm("ISO file is required"))
}
return
}
defer file.Close()
var progressFn image.ProgressFunc
if uploadID != "" {
progressFn = func(stage, detail string) {
u.Hub.Publish(events.Event{
Name: "image.upload_progress",
Payload: fmt.Sprintf(`{"upload_id":%q,"stage":%q,"detail":%q}`, uploadID, stage, detail),
})
}
}
_, err = u.ImageSvc.Upload(r.Context(), image.UploadParams{
Name: name,
Kind: kind,
Version: version,
ISO: file,
Name: name,
Kind: kind,
Version: version,
ISO: file,
OnProgress: progressFn,
})
if err != nil {
renderHTML(w, imageUploadForm(err.Error()))
if isXHR {
writeJSON(w, http.StatusInternalServerError, map[string]any{"ok": false, "error": err.Error()})
} else {
renderHTML(w, imageUploadForm(err.Error()))
}
return
}
http.Redirect(w, r, "/images", http.StatusSeeOther)
if isXHR {
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
} else {
http.Redirect(w, r, "/images", http.StatusSeeOther)
}
}
func (u *UI) SetDefaultImage(w http.ResponseWriter, r *http.Request) {