Browse Source

bug-fixes

master
Silberengel 4 weeks ago
parent
commit
95dac270e9
  1. 15
      internal/generator/html.go
  2. 15
      internal/nostr/client.go
  3. 25
      internal/nostr/ebooks.go
  4. 76
      internal/nostr/feed.go
  5. 11
      internal/nostr/naddr.go
  6. 4
      internal/server/handlers.go
  7. 23
      static/css/main.css
  8. 6
      static/css/responsive.css
  9. 80
      templates/base.html
  10. 4
      templates/blog.html
  11. 12
      templates/components.html
  12. 2
      templates/ebooks.html

15
internal/generator/html.go

@ -31,6 +31,20 @@ func getTemplateFuncs() template.FuncMap { @@ -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 @@ -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
},

15
internal/nostr/client.go

@ -255,6 +255,21 @@ func (c *Client) IsConnected() bool { @@ -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)

25
internal/nostr/ebooks.go

@ -142,10 +142,27 @@ func (es *EBooksService) FetchTopLevelIndexEvents(ctx context.Context) ([]EBookI @@ -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)
}

76
internal/nostr/feed.go

@ -3,6 +3,7 @@ package nostr @@ -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 { @@ -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

11
internal/nostr/naddr.go

@ -69,3 +69,14 @@ func (n *Naddr) ToFilter() nostr.Filter { @@ -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
}

4
internal/server/handlers.go

@ -364,8 +364,8 @@ func (s *Server) middleware(next http.Handler) http.Handler { @@ -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()

23
static/css/main.css

@ -43,6 +43,8 @@ body { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 {

6
static/css/responsive.css

@ -33,16 +33,18 @@ @@ -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 {

80
templates/base.html

@ -74,7 +74,7 @@ @@ -74,7 +74,7 @@
{{block "content" .}}{{end}}
</main>
{{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)}}
<aside class="feed-sidebar" aria-label="Recent notes">
{{template "feed" .}}
</aside>
@ -87,12 +87,53 @@ @@ -87,12 +87,53 @@
<script>
// Initialize Lucide icons
let iconInitInProgress = false;
let iconInitTimeout = null;
function initIcons() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
if (iconInitInProgress || typeof lucide === 'undefined') {
return;
}
iconInitInProgress = true;
try {
// Only initialize icons on elements that haven't been processed yet
// Lucide marks processed elements, so we can target unprocessed ones
const unprocessedIcons = document.querySelectorAll('[data-lucide]:not([data-lucide-processed])');
if (unprocessedIcons.length > 0) {
lucide.createIcons();
// Mark as processed to prevent re-processing
unprocessedIcons.forEach(el => {
el.setAttribute('data-lucide-processed', 'true');
});
}
} catch (e) {
console.error('Error initializing Lucide icons:', e);
} finally {
iconInitInProgress = false;
}
}
// Debounced icon initialization to prevent excessive calls
function debouncedInitIcons() {
if (iconInitTimeout) {
clearTimeout(iconInitTimeout);
}
iconInitTimeout = setTimeout(function() {
if (!iconInitInProgress) {
initIcons();
}
}, 200);
}
// Initialize icons on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(initIcons, 100);
});
} else {
setTimeout(initIcons, 100);
}
initIcons();
// Mobile menu toggle (optional - menu works without JS)
if (document.querySelector('.mobile-menu-toggle')) {
@ -105,10 +146,37 @@ @@ -105,10 +146,37 @@
}
// Reinitialize icons when DOM changes (for dynamically added content)
// Only observe for new elements with data-lucide that haven't been processed
const observer = new MutationObserver(function(mutations) {
initIcons();
let hasNewIcons = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) { // Element node
// Check if this node or its children have unprocessed icons
if (node.hasAttribute && node.hasAttribute('data-lucide') && !node.hasAttribute('data-lucide-processed')) {
hasNewIcons = true;
break;
}
if (node.querySelector && node.querySelector('[data-lucide]:not([data-lucide-processed])')) {
hasNewIcons = true;
break;
}
}
}
}
if (hasNewIcons) break;
}
if (hasNewIcons && !iconInitInProgress) {
debouncedInitIcons();
}
});
// Only observe for child additions, not attribute changes (to avoid Lucide's own DOM changes)
observer.observe(document.body, {
childList: true,
subtree: true
});
observer.observe(document.body, { childList: true, subtree: true });
</script>
</body>
</html>

4
templates/blog.html

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
<aside class="blog-sidebar">
<div class="blog-header">
{{if .BlogIndexAuthor}}
<div class="blog-author-handle">{{template "user-badge-simple" .BlogIndexAuthor}}</div>
<div class="blog-author-handle">{{template "user-badge-simple" (dict "Pubkey" .BlogIndexAuthor "Profiles" $.Profiles)}}</div>
{{end}}
{{if .BlogIndexImage}}
<div class="blog-image">
@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
<div class="article-link-meta">
<span class="article-date"><i data-lucide="clock" class="icon-inline"></i> {{$item.Time}}</span>
{{if $item.Author}}
<span class="article-author">{{template "user-badge-simple" $item.Author}}</span>
<span class="article-author">{{template "user-badge-simple" (dict "Pubkey" $item.Author "Profiles" $.Profiles)}}</span>
{{end}}
</div>
{{end}}

12
templates/components.html

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
{{range .FeedItems}}
<article class="feed-item">
<header class="feed-header">
<span class="feed-author">{{template "user-badge-simple" .Author}}</span>
<span class="feed-author">{{template "user-badge-simple" (dict "Pubkey" .Author "Profiles" $.Profiles)}}</span>
<time class="feed-time" datetime="{{.TimeISO}}"><i data-lucide="clock" class="icon-inline"></i> {{.Time}}</time>
</header>
<div class="feed-content">{{.Content}}</div>
@ -76,10 +76,14 @@ @@ -76,10 +76,14 @@
</span>
{{end}}
{{/* Simple user badge - takes a pubkey string and looks up profile from .Profiles map */}}
{{/* Simple user badge - takes a pubkey string and looks up profile from parent context's Profiles map
Usage: {{template "user-badge-simple" (dict "Pubkey" .Author "Profiles" $.Profiles)}}
Or with root context: {{template "user-badge-simple" (dict "Pubkey" .Author "Profiles" $)}}
*/}}
{{define "user-badge-simple"}}
{{$pubkey := .}}
{{$profile := index $.Profiles $pubkey}}
{{$pubkey := .Pubkey}}
{{$profiles := .Profiles}}
{{$profile := index $profiles $pubkey}}
<span class="user-badge" title="{{$pubkey}}">
{{if and $profile $profile.Picture}}
<img src="{{$profile.Picture}}" alt="{{if $profile.DisplayName}}{{$profile.DisplayName}}{{else if $profile.Name}}{{$profile.Name}}{{else}}User{{end}}" class="user-badge-avatar" loading="lazy">

2
templates/ebooks.html

@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
<strong>{{.Title}}</strong>
{{if .Summary}}<br><small class="text-muted">{{.Summary}}</small>{{end}}
</td>
<td>{{template "user-badge-simple" .Author}}</td>
<td>{{template "user-badge-simple" (dict "Pubkey" .Author "Profiles" $.Profiles)}}</td>
<td>{{if .Type}}{{.Type}}{{else}}—{{end}}</td>
<td>
<time datetime="{{.TimeISO}}">{{.Time}}</time>

Loading…
Cancel
Save