Improve update integration diagnostics

This commit is contained in:
rcourtman 2025-11-12 22:27:05 +00:00
parent 88d0aeebdf
commit bb55144637
8 changed files with 95 additions and 17 deletions

View file

@ -140,7 +140,7 @@ function GlobalUpdateProgressWatcher() {
} else if (!inProgress && hasAutoOpened()) {
setHasAutoOpened(false);
}
} catch (error) {
} catch (_error) {
// Silently ignore polling errors
}
};

View file

@ -2061,7 +2061,7 @@ const Settings: Component<SettingsProps> = (props) => {
showError('Invalid backup file format. Expected encrypted data in "data" field.');
return;
}
} catch (parseError) {
} catch (_parseError) {
// Not JSON - treat entire contents as raw base64 from CLI export
encryptedData = fileContent.trim();
}

View file

@ -19,7 +19,7 @@ const getInitialViewMode = (): MetricsViewMode => {
if (stored === 'sparklines' || stored === 'bars') {
return stored;
}
} catch (err) {
} catch (_err) {
// Ignore localStorage errors
}

View file

@ -188,6 +188,13 @@ func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return hijacker.Hijack()
}
// Flush implements http.Flusher when the underlying writer supports it.
func (rw *responseWriter) Flush() {
if flusher, ok := rw.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
// NewAPIError creates a new API error
func NewAPIError(statusCode int, code, message string) error {
return &APIError{

View file

@ -15,6 +15,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
@ -58,7 +59,11 @@ type UpdateInfo struct {
Warning string `json:"warning,omitempty"`
}
var errGitHubRateLimited = errors.New("GitHub API rate limit exceeded")
var (
errGitHubRateLimited = errors.New("GitHub API rate limit exceeded")
stageDelayOnce sync.Once
stageDelayValue time.Duration
)
// Manager handles update operations
type Manager struct {
@ -1137,6 +1142,10 @@ func (m *Manager) updateStatus(status string, progress int, message string, err
if m.sseBroadcast != nil {
m.sseBroadcast.Broadcast(statusCopy)
}
if delay := statusDelayForStage(status); delay > 0 {
time.Sleep(delay)
}
}
// sseHeartbeatLoop sends periodic heartbeats to SSE clients
@ -1168,6 +1177,37 @@ func sanitizeError(err error) string {
return errMsg
}
func statusDelayForStage(status string) time.Duration {
delay := configuredStageDelay()
if delay == 0 {
return 0
}
switch status {
case "downloading", "verifying", "extracting", "backing-up", "applying":
return delay
default:
return 0
}
}
func configuredStageDelay() time.Duration {
stageDelayOnce.Do(func() {
value := strings.TrimSpace(os.Getenv("PULSE_UPDATE_STAGE_DELAY_MS"))
if value == "" {
return
}
ms, err := strconv.Atoi(value)
if err != nil || ms <= 0 {
log.Warn().Str("value", value).Msg("Invalid PULSE_UPDATE_STAGE_DELAY_MS, ignoring")
return
}
stageDelayValue = time.Duration(ms) * time.Millisecond
})
return stageDelayValue
}
// cleanupOldTempDirs removes old pulse-update-* temp directories from previous runs
func (m *Manager) cleanupOldTempDirs() {
// Check multiple locations where temp dirs might exist

View file

@ -181,7 +181,7 @@ func waitForCompletion(t *testing.T, client *http.Client, baseURL string, timeou
if time.Now().After(deadline) {
t.Fatalf("update did not complete within %s (last status: %+v)", timeout, status)
}
time.Sleep(500 * time.Millisecond)
time.Sleep(100 * time.Millisecond)
}
}

View file

@ -41,6 +41,7 @@ services:
# Mock mode for faster testing
- PULSE_MOCK_MODE=true
- PULSE_ALLOW_DOCKER_UPDATES=true
- PULSE_UPDATE_STAGE_DELAY_MS=250
# Pre-configure authentication to bypass first-run setup
- PULSE_AUTH_USER=admin
- PULSE_AUTH_PASS=admin

View file

@ -57,6 +57,15 @@ func getenvDefault(key, fallback string) string {
return fallback
}
func isChecksumFilename(name string) bool {
switch strings.ToLower(name) {
case "checksums.txt", "sha256sums", "sha256sums.txt":
return true
default:
return false
}
}
func (rl *rateLimiter) cleanup() {
rl.mu.Lock()
defer rl.mu.Unlock()
@ -122,6 +131,7 @@ func main() {
// In-memory storage for tarballs and checksums
tarballs := make(map[string][]byte)
checksums := make(map[string]string)
checksumEntries := make(map[string][]string)
latestTag := getenvDefault("MOCK_LATEST_VERSION", "v99.0.0")
prevTag := getenvDefault("MOCK_PREVIOUS_VERSION", "v98.5.0")
@ -169,6 +179,9 @@ func main() {
}
checksums[filename] = checksum
entry := fmt.Sprintf("%s %s", checksum, filename)
checksumEntries[version] = append(checksumEntries[version], entry)
checksumEntries["v"+version] = append(checksumEntries["v"+version], entry)
// Add download URLs to release
rel.Assets = []struct {
@ -242,27 +255,44 @@ func main() {
version := parts[0]
file := parts[1]
if file == "checksums.txt" {
if isChecksumFilename(file) {
// Generate checksums.txt
var buf bytes.Buffer
for fname, chksum := range checksums {
if strings.Contains(fname, version) {
buf.WriteString(fmt.Sprintf("%s %s\n", chksum, fname))
entries, ok := checksumEntries[version]
if !ok {
trimmed := strings.TrimPrefix(version, "v")
entries = checksumEntries[trimmed]
}
if len(entries) == 0 {
w.WriteHeader(http.StatusNotFound)
log.Printf("No checksums found for version %s", version)
return
}
for _, line := range entries {
buf.WriteString(line)
if !strings.HasSuffix(line, "\n") {
buf.WriteByte('\n')
}
}
w.Header().Set("Content-Type", "text/plain")
w.Write(buf.Bytes())
log.Printf("Served checksums for version %s", version)
log.Printf("Served checksums for version %s (requested %s)", version, file)
return
}
// Serve tarball
filename := fmt.Sprintf("pulse-%s-linux-amd64.tar.gz", version)
tarball, ok := tarballs[filename]
// Serve tarball (strictly match requested filename first)
tarball, ok := tarballs[file]
if !ok {
w.WriteHeader(http.StatusNotFound)
log.Printf("Tarball not found: %s", filename)
return
// Fallback to canonical filename derived from version (without leading v)
trimmedVersion := strings.TrimPrefix(version, "v")
canonical := fmt.Sprintf("pulse-%s-linux-amd64.tar.gz", trimmedVersion)
tarball, ok = tarballs[canonical]
if !ok {
w.WriteHeader(http.StatusNotFound)
log.Printf("Tarball not found: %s (canonical %s)", file, canonical)
return
}
file = canonical
}
// Mark as stale if requested
@ -274,7 +304,7 @@ func main() {
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Length", strconv.Itoa(len(tarball)))
w.Write(tarball)
log.Printf("Served tarball: %s", filename)
log.Printf("Served tarball: %s (version %s)", file, version)
})
// Health check