mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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" />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user