diff --git a/public/healthz.json b/public/healthz.json index 28bed0c..ee82a9c 100644 --- a/public/healthz.json +++ b/public/healthz.json @@ -2,7 +2,7 @@ "status": "ok", "service": "aitherboard", "version": "0.1.0", - "buildTime": "2026-02-04T09:37:24.895Z", + "buildTime": "2026-02-04T09:46:20.554Z", "gitCommit": "unknown", - "timestamp": 1770197844895 + "timestamp": 1770198380554 } \ No newline at end of file diff --git a/src/lib/components/layout/SearchBox.svelte b/src/lib/components/layout/SearchBox.svelte index 5e32a99..ba80690 100644 --- a/src/lib/components/layout/SearchBox.svelte +++ b/src/lib/components/layout/SearchBox.svelte @@ -13,22 +13,13 @@ let searchResults = $state>([]); let showResults = $state(false); let searchInput: HTMLInputElement | null = $state(null); + let searchTimeout: ReturnType | null = null; // Decode bech32 identifiers function decodeIdentifier(input: string): { type: 'event' | 'profile' | null; id: string | null; pubkey: string | null } { const trimmed = input.trim(); - // Check if it's a hex event ID (64 hex characters) - if (/^[0-9a-f]{64}$/i.test(trimmed)) { - return { type: 'event', id: trimmed.toLowerCase(), pubkey: null }; - } - - // Check if it's a hex pubkey (64 hex characters) - if (/^[0-9a-f]{64}$/i.test(trimmed)) { - return { type: 'profile', id: null, pubkey: trimmed.toLowerCase() }; - } - - // Check if it's a bech32 encoded format + // Check if it's a bech32 encoded format first (before hex) if (/^(note|nevent|naddr|npub|nprofile)1[a-z0-9]+$/i.test(trimmed)) { try { const decoded = nip19.decode(trimmed); @@ -55,6 +46,14 @@ } } + // Check if it's a hex identifier (64 hex characters) + // We can't distinguish between event IDs and pubkeys by format alone, + // so we'll try event ID first, then fall back to pubkey if event not found + if (/^[0-9a-f]{64}$/i.test(trimmed)) { + // Default to event ID - if not found, we can try as pubkey in performSearch + return { type: 'event', id: trimmed.toLowerCase(), pubkey: trimmed.toLowerCase() }; + } + return { type: null, id: null, pubkey: null }; } @@ -96,6 +95,10 @@ if (event) { searchResults = [{ event, matchType: 'Event ID' }]; + } else if (decoded.pubkey) { + // Event not found, but we have a pubkey - try as profile + handleProfileClick(decoded.pubkey); + return; } } else if (decoded.type === 'profile' && decoded.pubkey) { // Search for profile - navigate directly to profile page @@ -145,9 +148,16 @@ const target = e.target as HTMLInputElement; searchQuery = target.value; - // Debounce search + // Clear existing timeout + if (searchTimeout) { + clearTimeout(searchTimeout); + } + + // Debounce search - wait 300ms after user stops typing if (searchQuery.trim()) { - performSearch(); + searchTimeout = setTimeout(() => { + performSearch(); + }, 300); } else { searchResults = []; showResults = false; @@ -182,15 +192,26 @@ const target = e.target as HTMLElement; if (!target.closest('.search-box-container')) { showResults = false; + searchQuery = ''; } }; - document.addEventListener('click', handleClickOutside); + // Use capture phase to catch clicks before they bubble + document.addEventListener('click', handleClickOutside, true); return () => { - document.removeEventListener('click', handleClickOutside); + document.removeEventListener('click', handleClickOutside, true); }; } }); + + // Cleanup timeout on unmount + $effect(() => { + return () => { + if (searchTimeout) { + clearTimeout(searchTimeout); + } + }; + });
@@ -215,7 +236,7 @@ {#each searchResults as { event, matchType }} +
+
Example of {effectiveKind} event
+
{exampleJSON}
+
+
+ + {#if helpText.suggestedTags.length > 0} +
+ Suggested tags: +
    + {#each helpText.suggestedTags as tag} +
  • {tag}
  • + {/each} +
+
+ {/if} + + + +
+ + + {#if isUnknownKind} +
+ updateTag(index, 0, e.currentTarget.value)} - placeholder="Tag name" - class="tag-input" + id="custom-kind-id" + type="number" + bind:value={customKindId} + placeholder="Enter kind number" + class="kind-id-input" + min="0" + max="65535" /> - {#each tag.slice(1) as value, valueIndex} - updateTag(index, valueIndex + 1, e.currentTarget.value)} - placeholder="Tag value" - class="tag-input" - /> - {/each} - -
- {/each} - + {/if} +
+ + {#if !isKind30040} +
+ +
- - - -
- + {/if} + +
+
+ Tags +
+ {#each tags as tag, index (index)} +
+ updateTag(index, 0, e.currentTarget.value)} + placeholder="Tag name" + class="tag-input" + /> + {#each tag.slice(1) as value, valueIndex} + updateTag(index, valueIndex + 1, e.currentTarget.value)} + placeholder="Tag value" + class="tag-input" + /> + {/each} + + +
+ {/each} +
+
+ +
+
+
+ +
+ +
@@ -216,23 +567,197 @@ {/if} diff --git a/src/routes/cache/+page.svelte b/src/routes/cache/+page.svelte new file mode 100644 index 0000000..d0f966c --- /dev/null +++ b/src/routes/cache/+page.svelte @@ -0,0 +1,876 @@ + + +
+ +
+
+

Cache Manager

+ + {#if loading && !stats} +
+

Loading cache statistics...

+
+ {:else if stats} + +
+

Statistics

+
+
+
Total Events
+
{stats.totalEvents.toLocaleString()}
+
+
+
Cache Size
+
{formatBytes(stats.totalSize)}
+
+
+
Oldest Event
+
{stats.oldestEvent ? formatDate(stats.oldestEvent) : 'N/A'}
+
+
+
Newest Event
+
{stats.newestEvent ? formatDate(stats.newestEvent) : 'N/A'}
+
+
+ + + {#if stats.eventsByKind.size > 0} +
+

Events by Kind

+
+ {#each Array.from(stats.eventsByKind.entries()).sort((a, b) => b[1] - a[1]) as [kind, count]} +
+ {getKindName(kind)} ({kind}) + {count.toLocaleString()} + +
+ {/each} +
+
+ {/if} +
+ + +
+

Filters

+
+
+ + +
+ +
+ + { + // Debounce search + const timeout = setTimeout(() => handleFilterChange(), 500); + return () => clearTimeout(timeout); + }} + placeholder="Filter by pubkey..." + class="filter-input" + /> +
+ +
+ + { + const timeout = setTimeout(() => handleFilterChange(), 500); + return () => clearTimeout(timeout); + }} + placeholder="Search event ID or content..." + class="filter-input" + /> +
+
+
+ + +
+

Bulk Actions

+
+ + + +
+
+ + +
+

Cached Events ({events.length})

+ + {#if loading && events.length === 0} +
+

Loading events...

+
+ {:else if events.length === 0} +
+

No events found in cache.

+
+ {:else} +
+ {#each events as event (event.id)} +
+ +
+
+
+ ID: + {event.id.substring(0, 16)}... + View +
+
+ Kind: {getKindName(event.kind)} ({event.kind}) + Pubkey: {event.pubkey.substring(0, 16)}... + Created: {formatDate(event.created_at)} + Cached: {formatDate(event.cached_at / 1000)} +
+
+
+ +
+
+ + {#if expandedEvents.has(event.id)} +
+
+ {JSON.stringify(event, null, 2)} +
+ +
+ {:else} +
+

{event.content.substring(0, 200)}{event.content.length > 200 ? '...' : ''}

+ +
+ {/if} +
+ {/each} +
+ + {#if hasMore} +
+ +
+ {/if} + {/if} +
+ {/if} +
+
+ + diff --git a/src/routes/feed/+page.svelte b/src/routes/feed/+page.svelte index 6e59b3c..3ff2fe2 100644 --- a/src/routes/feed/+page.svelte +++ b/src/routes/feed/+page.svelte @@ -13,8 +13,13 @@
-
- +
+
+ +
+ + ✍️ +
@@ -24,4 +29,57 @@ max-width: var(--content-width); margin: 0 auto; } + + .feed-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + } + + .search-section { + flex: 1; + } + + .write-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + border: 1px solid var(--fog-border, #e5e7eb); + border-radius: 0.375rem; + background: var(--fog-post, #ffffff); + color: var(--fog-text, #1f2937); + text-decoration: none; + transition: all 0.2s; + min-width: 2.5rem; + min-height: 2.5rem; + flex-shrink: 0; + } + + :global(.dark) .write-button { + border-color: var(--fog-dark-border, #374151); + background: var(--fog-dark-post, #1f2937); + color: var(--fog-dark-text, #f9fafb); + } + + .write-button:hover { + background: var(--fog-highlight, #f3f4f6); + border-color: var(--fog-accent, #64748b); + } + + :global(.dark) .write-button:hover { + background: var(--fog-dark-highlight, #475569); + border-color: var(--fog-dark-accent, #94a3b8); + } + + .emoji { + font-size: 1.25rem; + line-height: 1; + } + + .emoji-grayscale { + filter: grayscale(100%); + opacity: 0.7; + } diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index 7a3e2e9..da0b466 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -5,12 +5,24 @@ import { nostrClient } from '../../lib/services/nostr/nostr-client.js'; import { sessionManager } from '../../lib/services/auth/session-manager.js'; import { onMount } from 'svelte'; + import { page } from '$app/stores'; let mode = $state<'select' | 'find' | 'create'>('select'); + let initialKind = $state(null); const isLoggedIn = $derived(sessionManager.isLoggedIn()); onMount(async () => { await nostrClient.initialize(); + + // Check for kind parameter in URL + const kindParam = $page.url.searchParams.get('kind'); + if (kindParam) { + const kind = parseInt(kindParam, 10); + if (!isNaN(kind)) { + initialKind = kind; + mode = 'create'; + } + } }); @@ -48,7 +60,7 @@ {:else if mode === 'create'}
- +
{/if}