diff --git a/internal/generator/html.go b/internal/generator/html.go index 61e30dc..c2b6f53 100644 --- a/internal/generator/html.go +++ b/internal/generator/html.go @@ -31,6 +31,20 @@ func getTemplateFuncs() template.FuncMap { "shortenPubkey": func(pubkey string) string { return nostr.ShortenPubkey(pubkey) }, + "dict": func(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, fmt.Errorf("dict requires an even number of arguments") + } + dict := make(map[string]interface{}) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, fmt.Errorf("dict keys must be strings") + } + dict[key] = values[i+1] + } + return dict, nil + }, } } @@ -463,6 +477,7 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event SiteURL: g.siteURL, CurrentYear: time.Now().Year(), WikiPages: []WikiPageInfo{}, // Will be populated if needed + BlogItems: []BlogItemInfo{}, // Empty - no blog items on contact page FeedItems: []FeedItemInfo{}, // Empty - feed only on landing page Profiles: make(map[string]*nostr.Profile), // Empty profiles for contact page }, diff --git a/internal/nostr/client.go b/internal/nostr/client.go index d481c1d..fae8217 100644 --- a/internal/nostr/client.go +++ b/internal/nostr/client.go @@ -255,6 +255,21 @@ func (c *Client) IsConnected() bool { return true // We connect on-demand } +// GetRelays returns all configured relay URLs (primary, fallback, additional fallback) +func (c *Client) GetRelays() []string { + relays := []string{} + if c.primaryRelay != "" { + relays = append(relays, c.primaryRelay) + } + if c.fallbackRelay != "" { + relays = append(relays, c.fallbackRelay) + } + if c.additionalFallback != "" { + relays = append(relays, c.additionalFallback) + } + return relays +} + // HealthCheck performs a health check on the relays func (c *Client) HealthCheck(ctx context.Context, timeout time.Duration) error { ctx, cancel := context.WithTimeout(ctx, timeout) diff --git a/internal/nostr/ebooks.go b/internal/nostr/ebooks.go index 39a701b..26b29e8 100644 --- a/internal/nostr/ebooks.go +++ b/internal/nostr/ebooks.go @@ -142,10 +142,27 @@ func (es *EBooksService) FetchTopLevelIndexEvents(ctx context.Context) ([]EBookI } } - // Build naddr - // Format: naddr1 + bech32 encoded (kind:pubkey:dtag:relay) - // For simplicity, we'll use the format: kind:pubkey:dtag - ebook.Naddr = fmt.Sprintf("%d:%s:%s", es.indexKind, ebook.Author, ebook.DTag) + // Build naddr - create proper bech32-encoded naddr + // Extract relay hints from event tags if available + var relays []string + for _, tag := range event.Tags { + if len(tag) > 0 && tag[0] == "relays" { + relays = append(relays, tag[1:]...) + } + } + // If no relays in tags, use the relay we fetched from + if len(relays) == 0 { + relays = []string{es.relayURL} + } + + naddr, err := CreateNaddr(es.indexKind, ebook.Author, ebook.DTag, relays) + if err != nil { + log.Printf("Failed to create naddr for ebook %s: %v", ebook.DTag, err) + // Fallback to kind:pubkey:dtag format + ebook.Naddr = fmt.Sprintf("%d:%s:%s", es.indexKind, ebook.Author, ebook.DTag) + } else { + ebook.Naddr = naddr + } ebooks = append(ebooks, ebook) } diff --git a/internal/nostr/feed.go b/internal/nostr/feed.go index 891dd42..e711722 100644 --- a/internal/nostr/feed.go +++ b/internal/nostr/feed.go @@ -3,6 +3,7 @@ package nostr import ( "context" "fmt" + "log" "time" "github.com/nbd-wtf/go-nostr" @@ -22,38 +23,71 @@ func NewFeedService(client *Client, feedKind int) *FeedService { } } -// FetchFeedItems fetches recent feed events from a specific relay -func (fs *FeedService) FetchFeedItems(ctx context.Context, relay string, maxEvents int) ([]FeedItem, error) { +// FetchFeedItems fetches recent feed events from the configured feed relay only, with retries +func (fs *FeedService) FetchFeedItems(ctx context.Context, feedRelay string, maxEvents int) ([]FeedItem, error) { + if feedRelay == "" { + return nil, fmt.Errorf("feed relay not configured") + } + filter := nostr.Filter{ Kinds: []int{fs.feedKind}, Limit: maxEvents, } logFilter(filter, fmt.Sprintf("feed (kind %d)", fs.feedKind)) - // Connect to the specific feed relay (not the default client relays) - relayConn, err := fs.client.ConnectToRelay(ctx, relay) - if err != nil { - return nil, fmt.Errorf("failed to connect to feed relay %s: %w", relay, err) - } - defer relayConn.Close() + const maxRetries = 3 + var lastErr error - events, err := relayConn.QuerySync(ctx, filter) - if err != nil { - return nil, fmt.Errorf("failed to fetch feed events from %s: %w", relay, err) - } + // Retry up to 3 times for the configured feed relay only + for attempt := 1; attempt <= maxRetries; attempt++ { + if attempt > 1 { + log.Printf("Retrying feed relay %s (attempt %d/%d)", feedRelay, attempt, maxRetries) + // Wait a bit before retrying (exponential backoff: 1s, 2s) + waitTime := time.Duration(attempt-1) * time.Second + time.Sleep(waitTime) + } + + // Create a context with timeout for this attempt + attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second) - items := make([]FeedItem, 0, len(events)) - for _, event := range events { - item := FeedItem{ - Author: event.PubKey, - Content: event.Content, - Time: time.Unix(int64(event.CreatedAt), 0), - Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", event.ID), + // Connect to the relay + relayConn, err := fs.client.ConnectToRelay(attemptCtx, feedRelay) + if err != nil { + log.Printf("Failed to connect to feed relay %s (attempt %d/%d): %v", feedRelay, attempt, maxRetries, err) + lastErr = err + cancel() + continue // Try next attempt } - items = append(items, item) + + // Try to fetch events + events, err := relayConn.QuerySync(attemptCtx, filter) + relayConn.Close() + cancel() + + if err != nil { + log.Printf("Failed to fetch feed events from %s (attempt %d/%d): %v", feedRelay, attempt, maxRetries, err) + lastErr = err + continue // Try next attempt + } + + // Success! Convert events to feed items + items := make([]FeedItem, 0, len(events)) + for _, event := range events { + item := FeedItem{ + Author: event.PubKey, + Content: event.Content, + Time: time.Unix(int64(event.CreatedAt), 0), + Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", event.ID), + } + items = append(items, item) + } + + log.Printf("Successfully fetched %d feed items from %s (attempt %d/%d)", len(items), feedRelay, attempt, maxRetries) + return items, nil } - return items, nil + // All retries failed + return nil, fmt.Errorf("failed to fetch feed from %s after %d retries (last error: %w)", feedRelay, maxRetries, lastErr) } // FeedItem represents a feed item diff --git a/internal/nostr/naddr.go b/internal/nostr/naddr.go index d58913e..c7a98db 100644 --- a/internal/nostr/naddr.go +++ b/internal/nostr/naddr.go @@ -69,3 +69,14 @@ func (n *Naddr) ToFilter() nostr.Filter { }, } } + +// CreateNaddr creates a bech32-encoded naddr from kind, pubkey, dtag, and optional relays +func CreateNaddr(kind int, pubkey string, dtag string, relays []string) (string, error) { + // nip19.EncodeEntity signature: (publicKey string, kind int, identifier string, relays []string) + naddr, err := nip19.EncodeEntity(pubkey, kind, dtag, relays) + if err != nil { + return "", fmt.Errorf("failed to encode naddr: %w", err) + } + + return naddr, nil +} diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 069a3a1..b7eb85d 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -364,8 +364,8 @@ func (s *Server) middleware(next http.Handler) http.Handler { w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") - // CSP header - w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;") + // CSP header - allow unpkg.com for Lucide icons + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;") // Log request start := time.Now() diff --git a/static/css/main.css b/static/css/main.css index c332017..2482939 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -43,6 +43,8 @@ body { min-height: 100vh; display: flex; flex-direction: column; + margin: 0; + padding: 0; padding-top: 80px; /* Space for fixed header */ } @@ -72,10 +74,15 @@ header { right: 0; width: 100%; z-index: 1000; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: 0; + padding: 0; } .navbar { padding: 1rem 0; + width: 100%; + margin: 0; } .nav-container { @@ -111,12 +118,24 @@ header { list-style: none; gap: 2rem; align-items: center; + margin: 0; + padding: 0; +} + +.nav-menu li { + margin: 0; + padding: 0; + list-style: none; } .nav-menu a { color: var(--text-primary); text-decoration: none; transition: color 0.2s; + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; } .nav-menu a:hover { @@ -168,9 +187,12 @@ header { list-style: none; min-width: 200px; padding: 0.5rem 0; + margin: 0; opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; + z-index: 1001; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .dropdown:hover .dropdown-menu, @@ -196,6 +218,7 @@ header { padding: 2rem 1rem; gap: 2rem; flex: 1; + min-height: calc(100vh - 80px - 4rem); /* Account for header and padding */ } .wiki-layout { diff --git a/static/css/responsive.css b/static/css/responsive.css index 2237f13..e27e8ce 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -33,16 +33,18 @@ .nav-menu { position: fixed; - top: 60px; + top: 80px; /* Match header height */ left: -100%; width: 100%; - height: calc(100vh - 60px); + height: calc(100vh - 80px); background: var(--bg-secondary); flex-direction: column; align-items: flex-start; padding: 2rem; transition: left 0.3s; border-top: 1px solid var(--border-color); + z-index: 999; + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2); } .nav-menu.active { diff --git a/templates/base.html b/templates/base.html index 1253ab2..3323f6d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -74,7 +74,7 @@ {{block "content" .}}{{end}} - {{if or (eq .CanonicalURL .SiteURL) (eq .CanonicalURL (printf "%s/" .SiteURL))}} + {{if and (or (eq .CanonicalURL .SiteURL) (eq .CanonicalURL (printf "%s/" .SiteURL))) (gt (len .FeedItems) 0)}} @@ -87,12 +87,53 @@ diff --git a/templates/blog.html b/templates/blog.html index 4b3e700..0a60a86 100644 --- a/templates/blog.html +++ b/templates/blog.html @@ -4,7 +4,7 @@