polish: Improve update detection edge cases and UX

- Add GHCR (GitHub Container Registry) token support for public images
- Clean up dockerUpdateFirstSeen tracking when containers are removed
- Improve UpdateIcon tooltip to show digest info
- Add cursor-help to indicate hoverable tooltip
This commit is contained in:
rcourtman
2025-12-27 18:14:27 +00:00
parent 5148040ac4
commit cf44b0cca6
3 changed files with 62 additions and 30 deletions
@@ -58,11 +58,20 @@ export const UpdateBadge: Component<UpdateBadgeProps> = (props) => {
export const UpdateIcon: Component<{ updateStatus?: DockerContainerUpdateStatus }> = (props) => {
const hasUpdate = () => props.updateStatus?.updateAvailable === true;
const getTooltip = () => {
if (!props.updateStatus) return 'Image update available';
const current = props.updateStatus.currentDigest?.slice(0, 12) || 'unknown';
const latest = props.updateStatus.latestDigest?.slice(0, 12) || 'unknown';
return `Update available\nCurrent: ${current}...\nLatest: ${latest}...`;
};
return (
<Show when={hasUpdate()}>
<span
class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
title={`Image update available`}
class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300 cursor-help"
title={getTooltip()}
>
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
+13
View File
@@ -4491,6 +4491,14 @@ func (m *Manager) cleanupDockerContainerAlerts(host models.DockerHost, seen map[
}
}
}
// Cleanup update tracking for removed containers
for resourceID := range m.dockerUpdateFirstSeen {
if strings.HasPrefix(resourceID, prefix) {
if _, exists := seen[resourceID]; !exists {
delete(m.dockerUpdateFirstSeen, resourceID)
}
}
}
m.mu.Unlock()
for _, alertID := range toClear {
@@ -4523,6 +4531,11 @@ func (m *Manager) clearDockerHostContainerAlerts(hostID string) {
delete(m.dockerLastExitCode, resourceID)
}
}
for resourceID := range m.dockerUpdateFirstSeen {
if strings.HasPrefix(resourceID, prefix) {
delete(m.dockerUpdateFirstSeen, resourceID)
}
}
m.mu.Unlock()
for _, alertID := range toClear {
+38 -28
View File
@@ -272,41 +272,51 @@ func (r *RegistryChecker) getAuthToken(ctx context.Context, registry, repository
// Docker Hub requires auth token even for public images
if registry == "registry-1.docker.io" {
tokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", repository)
return r.fetchAuthToken(ctx, tokenURL)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil)
if err != nil {
return "", err
}
resp, err := r.httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token request failed: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var tokenResp struct {
Token string `json:"token"`
}
if err := json.Unmarshal(body, &tokenResp); err != nil {
return "", err
}
return tokenResp.Token, nil
// GitHub Container Registry (ghcr.io) requires auth token for public images
if registry == "ghcr.io" {
tokenURL := fmt.Sprintf("https://ghcr.io/token?service=ghcr.io&scope=repository:%s:pull", repository)
return r.fetchAuthToken(ctx, tokenURL)
}
// For other registries, try anonymous access first
return "", nil
}
// fetchAuthToken fetches an auth token from a token endpoint.
func (r *RegistryChecker) fetchAuthToken(ctx context.Context, tokenURL string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil)
if err != nil {
return "", err
}
resp, err := r.httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token request failed: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var tokenResp struct {
Token string `json:"token"`
}
if err := json.Unmarshal(body, &tokenResp); err != nil {
return "", err
}
return tokenResp.Token, nil
}
func (r *RegistryChecker) getCached(key string) *cacheEntry {
r.cache.mu.RLock()
defer r.cache.mu.RUnlock()