fix: skip update check for source builds and show appropriate UI message

Source builds use commit hashes (main-c147fa1) not semantic versions
(v4.23.0), so update checks would always fail or show misleading
"Update Available" banners.

Changes:
- Add IsSourceBuild flag to VersionInfo struct
- Detect source builds via BUILD_FROM_SOURCE marker file
- Skip update check for source builds (like Docker)
- Update frontend to show "Built from source" message
- Disable manual update check button for source builds
- Return "source" deployment type for source builds

Backend:
- internal/updates/version.go: Add isSourceBuildEnvironment() detection
- internal/updates/manager.go: Skip check with appropriate message
- internal/api/types.go: Add isSourceBuild to API response
- internal/api/router.go: Include isSourceBuild in version endpoint

Frontend:
- src/api/updates.ts: Add isSourceBuild to VersionInfo type
- src/stores/updates.ts: Don't poll for updates on source builds
- src/components/Settings/Settings.tsx: Show "Built from source" message

Fixes the confusing "Update Available" banner for users who explicitly
chose --source to get latest main branch code.

Co-authored-by: Codex AI
This commit is contained in:
rcourtman 2025-10-21 10:08:00 +00:00
parent 0e0661eb68
commit 66b97333f7
7 changed files with 60 additions and 3 deletions

View file

@ -26,6 +26,7 @@ export interface VersionInfo {
runtime: string;
channel?: string;
isDocker: boolean;
isSourceBuild: boolean;
isDevelopment: boolean;
deploymentType?: string;
}

View file

@ -3257,9 +3257,13 @@ const Settings: Component<SettingsProps> = (props) => {
<button
type="button"
onClick={checkForUpdates}
disabled={checkingForUpdates() || versionInfo()?.isDocker}
disabled={
checkingForUpdates() ||
versionInfo()?.isDocker ||
versionInfo()?.isSourceBuild
}
class={`px-4 py-2 text-sm rounded-lg transition-colors flex items-center gap-2 ${
versionInfo()?.isDocker
versionInfo()?.isDocker || versionInfo()?.isSourceBuild
? 'bg-gray-100 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
@ -3284,6 +3288,15 @@ const Settings: Component<SettingsProps> = (props) => {
</div>
</Show>
<Show when={versionInfo()?.isSourceBuild}>
<div class="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p class="text-xs text-blue-800 dark:text-blue-200">
<strong>Built from source:</strong> Pull the latest code from git and
rebuild to update.
</p>
</div>
</Show>
<Show when={Boolean(updateInfo()?.warning)}>
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p class="text-xs text-amber-800 dark:text-amber-200">

View file

@ -100,7 +100,7 @@ const checkForUpdates = async (force = false): Promise<void> => {
}
// Don't check for updates in Docker or development builds
if (version.isDocker || version.isDevelopment) {
if (version.isDocker || version.isDevelopment || version.isSourceBuild) {
setUpdateAvailable(false);
setUpdateInfo(null);
return;

View file

@ -2244,6 +2244,7 @@ func (r *Router) handleVersion(w http.ResponseWriter, req *http.Request) {
Runtime: runtime.Version(),
Channel: "stable",
IsDocker: false,
IsSourceBuild: false,
IsDevelopment: true,
}
w.Header().Set("Content-Type", "application/json")
@ -2260,6 +2261,7 @@ func (r *Router) handleVersion(w http.ResponseWriter, req *http.Request) {
Runtime: versionInfo.Runtime,
Channel: versionInfo.Channel,
IsDocker: versionInfo.IsDocker,
IsSourceBuild: versionInfo.IsSourceBuild,
IsDevelopment: versionInfo.IsDevelopment,
DeploymentType: versionInfo.DeploymentType,
}

View file

@ -29,6 +29,7 @@ type VersionResponse struct {
Runtime string `json:"runtime,omitempty"`
Channel string `json:"channel,omitempty"`
IsDocker bool `json:"isDocker"`
IsSourceBuild bool `json:"isSourceBuild"`
IsDevelopment bool `json:"isDevelopment"`
DeploymentType string `json:"deploymentType,omitempty"`
UpdateAvailable bool `json:"updateAvailable"`

View file

@ -164,6 +164,23 @@ func (m *Manager) CheckForUpdatesWithChannel(ctx context.Context, channel string
return info, nil
}
// Skip update check for source builds
if currentInfo.IsSourceBuild {
info := &UpdateInfo{
Available: false,
CurrentVersion: currentInfo.Version,
LatestVersion: currentInfo.Version,
}
if useCache {
m.statusMu.Lock()
m.checkCache[channel] = info
m.cacheTime[channel] = time.Now()
m.statusMu.Unlock()
}
m.updateStatus("idle", 0, "Updates not available for source builds")
return info, nil
}
// Parse current version first
currentVer, err := ParseVersion(currentInfo.Version)
if err != nil {

View file

@ -28,6 +28,7 @@ type VersionInfo struct {
Runtime string `json:"runtime"`
Channel string `json:"channel,omitempty"`
IsDocker bool `json:"isDocker"`
IsSourceBuild bool `json:"isSourceBuild"`
IsDevelopment bool `json:"isDevelopment"`
DeploymentType string `json:"deploymentType"`
}
@ -128,6 +129,7 @@ func GetCurrentVersion() (*VersionInfo, error) {
Channel: detectChannelFromVersion(normalized),
IsDevelopment: isDev,
IsDocker: isDockerEnvironment(),
IsSourceBuild: isSourceBuildEnvironment(),
DeploymentType: GetDeploymentType(),
}
}
@ -287,6 +289,23 @@ func isDockerEnvironment() bool {
return false
}
// isSourceBuildEnvironment checks if running from a source build
func isSourceBuildEnvironment() bool {
markerPaths := []string{
"BUILD_FROM_SOURCE",
"/opt/pulse/BUILD_FROM_SOURCE",
filepath.Join(filepath.Dir(os.Args[0]), "BUILD_FROM_SOURCE"),
}
for _, path := range markerPaths {
if fileExists(path) {
return true
}
}
return false
}
// fileExists checks if a file exists
func fileExists(path string) bool {
// Use os.Stat instead of exec.Command for better performance and reliability
@ -301,6 +320,10 @@ func GetDeploymentType() string {
return "docker"
}
if isSourceBuildEnvironment() {
return "source"
}
// Check for ProxmoxVE LXC installation (has update command)
if fileExists("/bin/update") {
// Read file directly instead of using exec.Command