Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/internal/huma/handlers/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,16 @@ func (h *TemplateHandler) GetRegistries(ctx context.Context, _ *GetTemplateRegis
return nil, huma.Error500InternalServerError((&common.RegistryFetchError{Err: mapErr}).Error())
}

// Overlay the last fetch error from the in-memory tracker so the UI can
// display why a registry is not returning templates without requiring the
// user to check server logs.
fetchErrors := h.templateService.GetRegistryFetchErrors()
for i := range out {
if msg, ok := fetchErrors[out[i].ID]; ok {
out[i].LastFetchError = &msg
}
}

return &GetTemplateRegistriesOutput{
Body: base.ApiResponse[[]template.TemplateRegistry]{
Success: true,
Expand Down
21 changes: 21 additions & 0 deletions backend/internal/services/template_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type TemplateService struct {

registryMu sync.RWMutex
registryFetchMeta map[string]*registryFetchMeta
registryErrors map[string]string // last fetch error per registry ID, cleared on success

fsSyncMu sync.Mutex
lastFsSync time.Time
Expand Down Expand Up @@ -91,6 +92,7 @@ func NewTemplateService(ctx context.Context, db *database.DB, httpClient *http.C
settingsService: settingsService,
remoteCache: remoteCache{},
registryFetchMeta: make(map[string]*registryFetchMeta),
registryErrors: make(map[string]string),
}
service.safeHTTPClient = service.newSafeHTTPClientInternal()
return service
Expand Down Expand Up @@ -404,6 +406,18 @@ func (s *TemplateService) GetRegistries(ctx context.Context) ([]models.TemplateR
return registries, nil
}

// GetRegistryFetchErrors returns a snapshot of the last fetch error per registry ID.
// An absent entry means the registry fetched successfully (or has never been attempted).
func (s *TemplateService) GetRegistryFetchErrors() map[string]string {
s.registryMu.RLock()
defer s.registryMu.RUnlock()
out := make(map[string]string, len(s.registryErrors))
for k, v := range s.registryErrors {
out[k] = v
}
return out
}

func (s *TemplateService) CreateRegistry(ctx context.Context, registry *models.TemplateRegistry) error {
// Hydrate metadata if needed
if registry.Name == "" || registry.Description == "" {
Expand Down Expand Up @@ -534,9 +548,16 @@ func (s *TemplateService) loadRemoteTemplates(ctx context.Context) ([]models.Com
remoteTemplates, err := s.fetchRegistryTemplates(groupCtx, &reg)
if err != nil {
slog.WarnContext(groupCtx, "failed to fetch templates from registry", "registry", reg.Name, "url", reg.URL, "error", err)
s.registryMu.Lock()
s.registryErrors[reg.ID] = err.Error()
s.registryMu.Unlock()
return nil // Don't fail the whole group if one registry fails
}

s.registryMu.Lock()
delete(s.registryErrors, reg.ID)
s.registryMu.Unlock()

mu.Lock()
defer mu.Unlock()
for _, template := range remoteTemplates {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/types/template.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface TemplateRegistry {
description: string;
createdAt?: string;
updatedAt?: string;
lastFetchError?: string;
}

export interface Template {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { Snippet } from '$lib/components/ui/snippet';
import { m } from '$lib/paraglide/messages';
import type { TemplateRegistry } from '$lib/types/template.type';
import { TrashIcon, RegistryIcon, CommunityIcon, RefreshIcon, ExternalLinkIcon, AddIcon } from '$lib/icons';
import { TrashIcon, RegistryIcon, CommunityIcon, RefreshIcon, ExternalLinkIcon, AddIcon, AlertTriangleIcon } from '$lib/icons';

let {
registries,
Expand Down Expand Up @@ -76,6 +76,12 @@
{#if registry.description}
<p class="text-muted-foreground mt-1 text-sm">{registry.description}</p>
{/if}
{#if registry.lastFetchError}
<div class="mt-2 flex items-start gap-1.5 rounded-md border border-destructive/30 bg-destructive/10 px-2 py-1.5">
<AlertTriangleIcon class="text-destructive mt-0.5 size-3.5 shrink-0" />
<p class="text-destructive text-xs break-all">{registry.lastFetchError}</p>
</div>
{/if}
</div>
<div class="flex items-center gap-2 self-end sm:self-center">
<Switch
Expand Down
5 changes: 5 additions & 0 deletions types/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ type TemplateRegistry struct {
//
// Required: true
Enabled bool `json:"enabled"`

// LastFetchError is the error message from the most recent failed fetch, if any.
//
// Required: false
LastFetchError *string `json:"lastFetchError,omitempty"`
}

// TemplateContent contains a template with its associated content and metadata.
Expand Down
Loading