From 000b42ba797fc457f4dd9304b011a52eee29c82f Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 27 Feb 2026 21:20:24 +0100 Subject: [PATCH] bug-fixes Nostr-Signature: 99cb543f1e821f1b7df4bbde2b3da3ab3a09cda7a1e9a537fe1b8df79b19e8e8 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 762a7ea92457ce81cc5aae9bc644fb9d80f90c7500035fbb506f2f76a5942333b828cc8a59f7656b0e714b15a59158be0a671f51476be2e8eabe9731ced74bcb --- nostr/commit-signatures.jsonl | 1 + src/lib/services/nostr/releases-service.ts | 17 ++ .../repos/[npub]/[repo]/releases/+server.ts | 4 +- src/routes/repos/[npub]/[repo]/+page.svelte | 36 ++-- .../[npub]/[repo]/components/DocsTab.svelte | 181 ++++++++++++++++-- .../[repo]/components/DocsViewer.svelte | 17 +- .../[npub]/[repo]/components/TagsTab.svelte | 2 + .../dialogs/CreateReleaseDialog.svelte | 60 +++++- .../[npub]/[repo]/services/file-operations.ts | 16 +- .../[repo]/services/release-operations.ts | 8 +- .../repos/[npub]/[repo]/stores/repo-state.ts | 4 +- .../[npub]/[repo]/utils/content-renderer.ts | 88 ++++++++- 12 files changed, 381 insertions(+), 53 deletions(-) diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 5123260..a56fa82 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -110,3 +110,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772182112,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 12"]],"content":"Signed commit: refactor 12","id":"73671ae6535309f9eae164f7a3ec403b1bc818ef811b9692fd0122d0b72c2774","sig":"0df56b009f5afb77de334225ab30cff55586ac0cf48f5ee435428201a1e72dc357a0fb5e80ef196f5bd76d6d448056d25f0feab0b1bcbe45f9af1a2a0d5453ca"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772188835,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 13"]],"content":"Signed commit: refactor 13","id":"f41c8662dcbf1be408c560d11eda0890c40582a8ea8bb3220116e645cc6a2bb5","sig":"2b7b70089cecfa4652fe236fa586a6fe1b05c1c95434a160717cbf5ee2f37382cdd8e8f31d7b3a7576ee5264e9e70c7a8651591caaea0cd311d1be4c561d282f"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772193104,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"02dcdcda1083cffd91dbf8906716c2ae09f06f77ef8590802afecd85f0b3108a","sig":"13d2b30ed37af03fd47dc09536058babb4dc63d1cfc55b8f38651ffd6342abcddc840b543c085b047721e9102b2d07e3dae78ff31d5990c92c04410ef1efcd5b"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772220851,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"d98d2d6a6eb27ba36f19015f7d6969fe3925c40b23187d70ccc9b61141c6b4b7","sig":"8727e3015e38a78d7a6105c26e5b1469dc4d6d701e58d5d6c522ab529b4daa2d39d4353eb6d091f3c1fd28ad0289eae808494c9e2722bf9065dd2b2e9001664f"} diff --git a/src/lib/services/nostr/releases-service.ts b/src/lib/services/nostr/releases-service.ts index 2dc9d9c..e54c98d 100644 --- a/src/lib/services/nostr/releases-service.ts +++ b/src/lib/services/nostr/releases-service.ts @@ -10,9 +10,11 @@ import { signEventWithNIP07 } from './nip07-signer.js'; export interface Release extends NostrEvent { kind: typeof KIND.RELEASE; + title?: string; tagName: string; tagHash?: string; releaseNotes?: string; + downloadUrl?: string; isDraft?: boolean; isPrerelease?: boolean; } @@ -49,16 +51,20 @@ export class ReleasesService { // Parse release information from tags return releases.map(release => { + const title = release.tags.find(t => t[0] === 'title')?.[1]; const tagName = release.tags.find(t => t[0] === 'tag')?.[1] || ''; const tagHash = release.tags.find(t => t[0] === 'r' && t[2] === 'tag')?.[1]; + const downloadUrl = release.tags.find(t => t[0] === 'r' && t[2] === 'download')?.[1]; const isDraft = release.tags.some(t => t[0] === 'draft' && t[1] === 'true'); const isPrerelease = release.tags.some(t => t[0] === 'prerelease' && t[1] === 'true'); return { ...release, + title, tagName, tagHash, releaseNotes: release.content, + downloadUrl, isDraft, isPrerelease }; @@ -79,9 +85,11 @@ export class ReleasesService { async createRelease( repoOwnerPubkey: string, repoId: string, + title: string, tagName: string, tagHash: string, releaseNotes: string, + downloadUrl: string, isDraft: boolean = false, isPrerelease: boolean = false ): Promise { @@ -94,6 +102,14 @@ export class ReleasesService { ['r', tagHash, '', 'tag'] // Reference to the git tag commit ]; + if (title) { + tags.push(['title', title]); + } + + if (downloadUrl) { + tags.push(['r', downloadUrl, '', 'download']); // Download URL with marker + } + if (isDraft) { tags.push(['draft', 'true']); } @@ -120,6 +136,7 @@ export class ReleasesService { tagName, tagHash, releaseNotes, + downloadUrl, isDraft, isPrerelease }; diff --git a/src/routes/api/repos/[npub]/[repo]/releases/+server.ts b/src/routes/api/repos/[npub]/[repo]/releases/+server.ts index ba44c74..287b878 100644 --- a/src/routes/api/repos/[npub]/[repo]/releases/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/releases/+server.ts @@ -24,7 +24,7 @@ export const GET: RequestHandler = createRepoGetHandler( export const POST: RequestHandler = withRepoValidation( async ({ repoContext, requestContext, event }) => { const body = await event.request.json(); - const { tagName, tagHash, releaseNotes, isDraft, isPrerelease } = body; + const { title, tagName, tagHash, releaseNotes, downloadUrl, isDraft, isPrerelease } = body; if (!tagName || !tagHash) { throw handleValidationError('Missing required fields: tagName, tagHash', { operation: 'createRelease', npub: repoContext.npub, repo: repoContext.repo }); @@ -42,9 +42,11 @@ export const POST: RequestHandler = withRepoValidation( const release = await releasesService.createRelease( repoContext.repoOwnerPubkey, repoContext.repo, + title || '', tagName, tagHash, releaseNotes || '', + downloadUrl || '', isDraft || false, isPrerelease || false ); diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index c7aa601..30d5c8b 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -203,11 +203,13 @@ } from './services/file-operations.js'; // Consolidated state - all state variables in one object + // @ts-expect-error - $state rune type inference issue with circular reference let state = $state(createRepoState()); // Local variables for component-specific state let announcementEventId = { value: null as string | null }; let applying: Record = {}; + let docsReloadTrigger = $state(0); // Extract fields from announcement for convenience const repoAnnouncement = $derived(state.pageData.announcement); @@ -655,6 +657,9 @@ // Reload documentation await loadDocumentation(); + + // Trigger DocsTab reload + docsReloadTrigger++; alert('Documentation event created and published successfully!'); } catch (err) { @@ -916,10 +921,11 @@ // Decode npub to get repo owner pubkey for bookmark address try { if (state.npub && state.npub.trim()) { - const decoded = nip19.decode(state.npub); - if (decoded.type === 'npub') { - state.metadata.ownerPubkey = decoded.data as string; - state.metadata.address = `${KIND.REPO_ANNOUNCEMENT}:${state.metadata.ownerPubkey}:${state.repo}`; + const decoded = nip19.decode(state.npub) as { type: 'npub' | 'note' | 'nevent' | 'naddr' | 'nprofile'; data: string | unknown }; + // Type guard for npub - decoded.data is string for npub type + if (decoded.type === 'npub' && typeof decoded.data === 'string') { + state.metadata.ownerPubkey = decoded.data; + state.metadata.address = `${KIND.REPO_ANNOUNCEMENT}:${decoded.data}:${state.repo}`; } } } catch (err) { @@ -987,7 +993,7 @@ error: status.error || null, message: status.message || null, cloneCount: status.cloneVerifications?.length || 0, - verifiedClones: status.cloneVerifications?.filter(cv => cv.verified).length || 0 + verifiedClones: status.cloneVerifications?.filter((cv: { verified: boolean }) => cv.verified).length || 0 }); } @@ -1254,7 +1260,7 @@ {/if} {#each (state.clone.showAllUrls ? repoCloneUrls : repoCloneUrls.slice(0, 3)) as cloneUrl} - {@const cloneVerification = state.verification.status?.cloneVerifications?.find(cv => { + {@const cloneVerification = state.verification.status?.cloneVerifications?.find((cv: { url: string }) => { const normalizeUrl = (url: string) => url.replace(/\/$/, '').toLowerCase().replace(/^https?:\/\//, ''); const normalizedCv = normalizeUrl(cv.url); const normalizedClone = normalizeUrl(cloneUrl); @@ -1522,7 +1528,7 @@ state.git.verifyingCommits.add(hash); try { // Trigger verification logic - find the commit and verify - const commit = state.git.commits.find(c => (c.hash || (c as any).sha) === hash); + const commit = state.git.commits.find((c: { hash?: string; sha?: string }) => (c.hash || c.sha) === hash); if (commit) { await verifyCommit(hash); } @@ -1584,6 +1590,13 @@ onCreateRelease={(tagName, tagHash) => { state.forms.release.tagName = tagName; state.forms.release.tagHash = tagHash; + // Pre-fill download URL with full URL + if (typeof window !== 'undefined') { + const origin = window.location.origin; + state.forms.release.downloadUrl = `${origin}/api/repos/${state.npub}/${state.repo}/download?ref=${encodeURIComponent(tagName)}&format=zip`; + } else { + state.forms.release.downloadUrl = `/api/repos/${state.npub}/${state.repo}/download?ref=${encodeURIComponent(tagName)}&format=zip`; + } state.openDialog = 'createRelease'; }} onLoadTags={loadTags} @@ -1633,7 +1646,7 @@ }} onStatusUpdate={async (id, status) => { // Find issue and update status - const issue = state.issues.find(i => i.id === id); + const issue = state.issues.find((i: { id: string }) => i.id === id); if (issue) { await updateIssueStatus(id, issue.author, status as 'open' | 'closed' | 'resolved' | 'draft'); await loadIssues(); @@ -1671,7 +1684,7 @@ }} onStatusUpdate={async (id, status) => { // Find PR and update status - similar to updateIssueStatus - const pr = state.prs.find(p => p.id === id); + const pr = state.prs.find((p: { id: string }) => p.id === id); if (pr && state.user.pubkeyHex) { // Check if user is maintainer or PR author const isAuthor = state.user.pubkeyHex === pr.author; @@ -1732,7 +1745,7 @@ state.selected.patch = id; }} onStatusUpdate={async (id, status) => { - const patch = state.patches.find(p => p.id === id); + const patch = state.patches.find((p: { id: string }) => p.id === id); if (patch) { await updatePatchStatus(id, patch.author, status); } @@ -1740,7 +1753,7 @@ onApply={async (id) => { applying[id] = true; try { - const patch = state.patches.find(p => p.id === id); + const patch = state.patches.find((p: { id: string }) => p.id === id); if (!patch) { throw new Error('Patch not found'); } @@ -1865,6 +1878,7 @@ goto(url.pathname + url.search, { replaceState: true, noScroll: true }); }} isMaintainer={state.maintainers.isMaintainer} + reloadTrigger={docsReloadTrigger} onCreateDocumentation={() => { if (!state.user.pubkey || !state.maintainers.isMaintainer) return; state.openDialog = 'createDocumentation'; diff --git a/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte b/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte index ab28777..c083915 100644 --- a/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte @@ -6,11 +6,13 @@ import TabLayout from './TabLayout.svelte'; import DocsViewer from './DocsViewer.svelte'; + import EventCopyButton from '$lib/components/EventCopyButton.svelte'; import type { NostrEvent } from '$lib/types/nostr.js'; import { KIND } from '$lib/types/nostr.js'; import { NostrClient } from '$lib/services/nostr/nostr-client.js'; import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import logger from '$lib/services/logger.js'; + import { extractAsciiDocTitle } from '../utils/content-renderer.js'; interface Props { npub?: string; @@ -22,6 +24,7 @@ onTabChange?: (tab: string) => void; isMaintainer?: boolean; onCreateDocumentation?: () => void; + reloadTrigger?: number; // Changes when documentation is created to trigger reload } let { @@ -33,11 +36,13 @@ tabs = [], onTabChange = () => {}, isMaintainer = false, - onCreateDocumentation = () => {} + onCreateDocumentation = () => {}, + reloadTrigger = 0 }: Props = $props(); let documentationContent = $state(null); let documentationKind = $state<'markdown' | 'asciidoc' | 'text' | '30040' | null>(null); + let documentationTitle = $state(null); let indexEvent = $state(null); let loading = $state(false); let loadingDocs = $state(false); @@ -45,12 +50,20 @@ let docFiles: Array<{ name: string; path: string }> = $state([]); let selectedDoc: string | null = $state(null); let hasReadme = $state(false); + let nostrDocs: Array<{ id: string; title: string; kind: number; event: NostrEvent }> = $state([]); $effect(() => { if (npub && repo && currentBranch) { loadDocumentation(); } }); + + // Reload when reloadTrigger changes (e.g., after creating documentation) + $effect(() => { + if (reloadTrigger > 0 && npub && repo && currentBranch) { + loadDocumentation(); + } + }); async function loadDocumentation() { loading = true; @@ -58,6 +71,7 @@ error = null; documentationContent = null; documentationKind = null; + documentationTitle = null; // Clear any previous title indexEvent = null; hasReadme = false; @@ -106,6 +120,9 @@ } } + // Load Nostr documentation events (kind 30818, 30041, 30817, 30023) + await loadNostrDocumentation(); + } catch (err) { error = err instanceof Error ? err.message : 'Failed to load documentation'; logger.error({ error: err, npub, repo }, 'Error loading documentation'); @@ -120,6 +137,10 @@ async function loadDocFile(path: string) { try { + // Clear any Nostr doc state when loading a file + documentationTitle = null; + indexEvent = null; + const response = await fetch(`/api/repos/${npub}/${repo}/raw?path=${encodeURIComponent(path)}&ref=${currentBranch || 'HEAD'}`); if (response.ok) { const content = await response.text(); @@ -131,6 +152,11 @@ documentationKind = 'markdown'; } else if (ext === 'adoc' || ext === 'asciidoc') { documentationKind = 'asciidoc'; + // Extract title for AsciiDoc files too + const extractedTitle = extractAsciiDocTitle(content); + if (extractedTitle) { + documentationTitle = extractedTitle; + } } else { documentationKind = 'text'; } @@ -167,6 +193,71 @@ logger.debug({ error: err }, 'No kind 30040 index found or error checking'); } } + + async function loadNostrDocumentation() { + try { + const { requireNpubHex } = await import('$lib/utils/npub-utils.js'); + const repoOwnerPubkey = requireNpubHex(npub); + const repoAddress = `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkey}:${repo}`; + + const client = new NostrClient(relays); + // Load documentation events: 30818 (Repository State), 30041 (Publication), 30817 (Repository Announcement), 30023 (Article) + const events = await client.fetchEvents([ + { + kinds: [30818, 30041, 30817, 30023], + authors: [repoOwnerPubkey], + '#a': [repoAddress], + limit: 100 + } + ]); + + nostrDocs = events.map(event => { + const titleTag = event.tags.find(t => t[0] === 'title'); + const title = titleTag?.[1] || `Documentation (kind ${event.kind})`; + return { + id: event.id, + title, + kind: event.kind, + event + }; + }); + + logger.debug({ count: nostrDocs.length }, 'Loaded Nostr documentation events'); + } catch (err) { + logger.debug({ error: err }, 'Error loading Nostr documentation events'); + } + } + + function loadNostrDoc(doc: { id: string; title: string; kind: number; event: NostrEvent }) { + // Clear previous content first to ensure re-render + documentationContent = null; + documentationKind = null; + documentationTitle = null; + + // Set new content and type + documentationContent = doc.event.content; + // Determine content type based on kind + if (doc.kind === 30041 || doc.kind === 30818) { + documentationKind = 'asciidoc'; + // Extract document title from AsciiDoc + const extractedTitle = extractAsciiDocTitle(doc.event.content); + documentationTitle = extractedTitle || doc.title; + } else { + documentationKind = 'markdown'; + documentationTitle = doc.title; + } + selectedDoc = `nostr:${doc.id}`; + indexEvent = null; // Clear index event if viewing a doc + + logger.debug({ + docId: doc.id, + kind: doc.kind, + contentType: documentationKind, + title: documentationTitle, + contentLength: doc.event.content.length, + contentPreview: doc.event.content.substring(0, 50) + }, 'Loading Nostr documentation'); + } function handleItemClick(item: any) { if (item.url) { @@ -210,12 +301,11 @@ + + + {/each} + {/if} + {#if !hasReadme && docFiles.length === 0 && nostrDocs.length === 0}

No documentation files found

@@ -257,14 +365,21 @@ onItemClick={handleItemClick} /> {:else if documentationContent} - +
+ {#if documentationTitle} +
+

{documentationTitle}

+
+ {/if} + +
{:else}

No documentation found

@@ -334,6 +449,18 @@ flex: 1; overflow-y: auto; } + + .nostr-doc-item { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + } + + .nostr-doc-item .doc-item { + flex: 1; + margin-bottom: 0; + } .doc-item { width: 100%; @@ -394,4 +521,26 @@ border-radius: 4px; margin: 1rem; } + + .docs-panel { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + box-sizing: border-box; + } + + .docs-panel-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + margin-bottom: 1rem; + flex-shrink: 0; + } + + .docs-panel-title { + margin: 0; + font-size: 1.5rem; + font-weight: 600; + color: var(--text-primary); + } diff --git a/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte b/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte index 2541568..0139b17 100644 --- a/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte +++ b/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte @@ -89,12 +89,17 @@ let error = $state(null); $effect(() => { - if (contentType === '30040' && indexEvent) { + // Explicitly track both content and contentType + const currentContent = content; + const currentContentType = contentType; + + if (currentContentType === '30040' && indexEvent) { // Publication index - handled by PublicationIndexViewer return; } - if (content) { + if (currentContent) { + // Re-render when content or contentType changes doRenderContent(); } }); @@ -104,7 +109,7 @@ error = null; try { - logger.operation('Rendering content', { contentType, length: content.length }); + logger.operation('Rendering content', { contentType, length: content.length, preview: content.substring(0, 100) }); // Use the shared content renderer utility // contentType '30040' is handled separately by PublicationIndexViewer @@ -114,16 +119,18 @@ } else { renderedContent = await renderContent(content, contentType as 'markdown' | 'asciidoc' | 'text'); + logger.debug({ contentType, renderedLength: renderedContent.length, preview: renderedContent.substring(0, 200) }, 'Content rendered'); + // Rewrite image paths to use API endpoint if (npub && repo && filePath) { renderedContent = rewriteImagePaths(renderedContent, filePath, npub, repo, currentBranch); } } - logger.operation('Content rendered', { contentType }); + logger.operation('Content rendered', { contentType, renderedLength: renderedContent.length }); } catch (err) { error = err instanceof Error ? err.message : 'Failed to render content'; - logger.error({ error: err, contentType }, 'Error rendering content'); + logger.error({ error: err, contentType, contentPreview: content.substring(0, 100) }, 'Error rendering content'); } finally { loading = false; } diff --git a/src/routes/repos/[npub]/[repo]/components/TagsTab.svelte b/src/routes/repos/[npub]/[repo]/components/TagsTab.svelte index d86b6aa..980164c 100644 --- a/src/routes/repos/[npub]/[repo]/components/TagsTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/TagsTab.svelte @@ -10,9 +10,11 @@ tags: Array<{ name: string; hash: string; message?: string; date?: number }>; releases: Array<{ id: string; + title?: string; tagName: string; tagHash?: string; releaseNotes?: string; + downloadUrl?: string; isDraft?: boolean; isPrerelease?: boolean; created_at: number; diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte index c648812..34c32f7 100644 --- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte +++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte @@ -13,6 +13,10 @@ + + -