Browse Source

bug-fixes

Nostr-Signature: 99cb543f1e821f1b7df4bbde2b3da3ab3a09cda7a1e9a537fe1b8df79b19e8e8 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 762a7ea92457ce81cc5aae9bc644fb9d80f90c7500035fbb506f2f76a5942333b828cc8a59f7656b0e714b15a59158be0a671f51476be2e8eabe9731ced74bcb
main
Silberengel 2 weeks ago
parent
commit
000b42ba79
  1. 1
      nostr/commit-signatures.jsonl
  2. 17
      src/lib/services/nostr/releases-service.ts
  3. 4
      src/routes/api/repos/[npub]/[repo]/releases/+server.ts
  4. 36
      src/routes/repos/[npub]/[repo]/+page.svelte
  5. 163
      src/routes/repos/[npub]/[repo]/components/DocsTab.svelte
  6. 17
      src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte
  7. 2
      src/routes/repos/[npub]/[repo]/components/TagsTab.svelte
  8. 60
      src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte
  9. 16
      src/routes/repos/[npub]/[repo]/services/file-operations.ts
  10. 6
      src/routes/repos/[npub]/[repo]/services/release-operations.ts
  11. 4
      src/routes/repos/[npub]/[repo]/stores/repo-state.ts
  12. 78
      src/routes/repos/[npub]/[repo]/utils/content-renderer.ts

1
nostr/commit-signatures.jsonl

@ -110,3 +110,4 @@ @@ -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"}

17
src/lib/services/nostr/releases-service.ts

@ -10,9 +10,11 @@ import { signEventWithNIP07 } from './nip07-signer.js'; @@ -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 { @@ -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 { @@ -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<Release> {
@ -94,6 +102,14 @@ export class ReleasesService { @@ -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 { @@ -120,6 +136,7 @@ export class ReleasesService {
tagName,
tagHash,
releaseNotes,
downloadUrl,
isDraft,
isPrerelease
};

4
src/routes/api/repos/[npub]/[repo]/releases/+server.ts

@ -24,7 +24,7 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -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( @@ -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
);

36
src/routes/repos/[npub]/[repo]/+page.svelte

@ -203,11 +203,13 @@ @@ -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<string, boolean> = {};
let docsReloadTrigger = $state(0);
// Extract fields from announcement for convenience
const repoAnnouncement = $derived(state.pageData.announcement);
@ -656,6 +658,9 @@ @@ -656,6 +658,9 @@
// Reload documentation
await loadDocumentation();
// Trigger DocsTab reload
docsReloadTrigger++;
alert('Documentation event created and published successfully!');
} catch (err) {
console.error('Failed to create documentation:', err);
@ -916,10 +921,11 @@ @@ -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 @@ @@ -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 @@ @@ -1254,7 +1260,7 @@
</button>
{/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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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';

163
src/routes/repos/[npub]/[repo]/components/DocsTab.svelte

@ -6,11 +6,13 @@ @@ -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 @@ @@ -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 @@ @@ -33,11 +36,13 @@
tabs = [],
onTabChange = () => {},
isMaintainer = false,
onCreateDocumentation = () => {}
onCreateDocumentation = () => {},
reloadTrigger = 0
}: Props = $props();
let documentationContent = $state<string | null>(null);
let documentationKind = $state<'markdown' | 'asciidoc' | 'text' | '30040' | null>(null);
let documentationTitle = $state<string | null>(null);
let indexEvent = $state<NostrEvent | null>(null);
let loading = $state(false);
let loadingDocs = $state(false);
@ -45,6 +50,7 @@ @@ -45,6 +50,7 @@
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) {
@ -52,12 +58,20 @@ @@ -52,12 +58,20 @@
}
});
// Reload when reloadTrigger changes (e.g., after creating documentation)
$effect(() => {
if (reloadTrigger > 0 && npub && repo && currentBranch) {
loadDocumentation();
}
});
async function loadDocumentation() {
loading = true;
loadingDocs = true;
error = null;
documentationContent = null;
documentationKind = null;
documentationTitle = null; // Clear any previous title
indexEvent = null;
hasReadme = false;
@ -106,6 +120,9 @@ @@ -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 @@ @@ -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 @@ @@ -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';
}
@ -168,6 +194,71 @@ @@ -168,6 +194,71 @@
}
}
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) {
window.open(item.url, '_blank');
@ -210,12 +301,11 @@ @@ -210,12 +301,11 @@
<button
class="doc-item {selectedDoc === 'README.md' ? 'selected' : ''}"
onclick={() => {
// Reload README if needed
if (!documentationContent) {
// Always reload README to ensure we have the right content
// Clear any Nostr doc state first
documentationTitle = null;
indexEvent = null;
loadDocumentation();
} else {
selectedDoc = 'README.md';
}
}}
>
README.md
@ -234,7 +324,25 @@ @@ -234,7 +324,25 @@
</li>
{/each}
{/if}
{#if !hasReadme && docFiles.length === 0}
{#if nostrDocs.length > 0}
{#each nostrDocs as doc}
<li class="nostr-doc-item">
<button
class="doc-item {selectedDoc === `nostr:${doc.id}` ? 'selected' : ''}"
onclick={() => loadNostrDoc(doc)}
title="Kind {doc.kind}"
>
{doc.title}
</button>
<EventCopyButton
eventId={doc.id}
kind={doc.kind}
pubkey={doc.event.pubkey}
/>
</li>
{/each}
{/if}
{#if !hasReadme && docFiles.length === 0 && nostrDocs.length === 0}
<div class="empty-sidebar">
<p>No documentation files found</p>
</div>
@ -257,6 +365,12 @@ @@ -257,6 +365,12 @@
onItemClick={handleItemClick}
/>
{:else if documentationContent}
<div class="docs-panel">
{#if documentationTitle}
<div class="docs-panel-header">
<h2 class="docs-panel-title">{documentationTitle}</h2>
</div>
{/if}
<DocsViewer
content={documentationContent}
contentType={documentationKind || 'text'}
@ -265,6 +379,7 @@ @@ -265,6 +379,7 @@
currentBranch={currentBranch || 'HEAD'}
filePath={selectedDoc || 'README.md'}
/>
</div>
{:else}
<div class="empty-docs">
<p>No documentation found</p>
@ -335,6 +450,18 @@ @@ -335,6 +450,18 @@
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%;
padding: 0.75rem;
@ -394,4 +521,26 @@ @@ -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);
}
</style>

17
src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte

@ -89,12 +89,17 @@ @@ -89,12 +89,17 @@
let error = $state<string | null>(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 @@ @@ -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 @@ @@ -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;
}

2
src/routes/repos/[npub]/[repo]/components/TagsTab.svelte

@ -10,9 +10,11 @@ @@ -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;

60
src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte

@ -13,6 +13,10 @@ @@ -13,6 +13,10 @@
</script>
<Modal {open} title="Create New Release" ariaLabel="Create new release" {onClose}>
<label>
Title:
<input type="text" bind:value={state.forms.release.title} placeholder="Release title (e.g., Version 1.0.0)" />
</label>
<label>
Tag Name:
<input type="text" bind:value={state.forms.release.tagName} placeholder="v1.0.0" />
@ -21,17 +25,28 @@ @@ -21,17 +25,28 @@
Tag Hash (commit hash):
<input type="text" bind:value={state.forms.release.tagHash} placeholder="abc1234..." />
</label>
<label>
Download URL (optional):
<input type="url" bind:value={state.forms.release.downloadUrl} placeholder="/api/repos/.../download?ref=..." />
<small class="field-hint">Pre-filled with the ZIP download URL for this tag. You can change it if needed.</small>
</label>
<label>
Release Notes:
<textarea bind:value={state.forms.release.notes} rows="10" placeholder="Release notes in markdown..."></textarea>
</label>
<label>
<label class="checkbox-label">
<input type="checkbox" bind:checked={state.forms.release.isDraft} />
Draft Release
<div class="checkbox-content">
<span class="checkbox-title">Draft Release</span>
<span class="checkbox-explanation">Published but marked as draft. Viewers can filter it out, but it's still visible on Nostr relays.</span>
</div>
</label>
<label>
<label class="checkbox-label">
<input type="checkbox" bind:checked={state.forms.release.isPrerelease} />
Pre-release
<div class="checkbox-content">
<span class="checkbox-title">Pre-release</span>
<span class="checkbox-explanation">Marks this as a pre-release (alpha, beta, or release candidate), not the final stable version.</span>
</div>
</label>
<div class="modal-actions">
<button onclick={onClose} class="cancel-button">Cancel</button>
@ -65,6 +80,43 @@ @@ -65,6 +80,43 @@
margin-right: 0.5rem;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.checkbox-label input[type="checkbox"] {
margin-top: 0.25rem;
flex-shrink: 0;
}
.checkbox-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}
.checkbox-title {
font-weight: 500;
color: var(--text-primary);
}
.checkbox-explanation {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.4;
}
.field-hint {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: var(--text-secondary);
font-style: italic;
}
.modal-actions {
display: flex;
gap: 0.5rem;

16
src/routes/repos/[npub]/[repo]/services/file-operations.ts

@ -75,7 +75,7 @@ export async function saveFile( @@ -75,7 +75,7 @@ export async function saveFile(
await apiPost(`/api/repos/${state.npub}/${state.repo}/file`, {
path: state.files.currentFile,
content: state.files.editedContent,
message: state.forms.commit.message.trim(),
commitMessage: state.forms.commit.message.trim(),
authorName: authorName,
authorEmail: authorEmail,
branch: state.git.currentBranch,
@ -153,7 +153,7 @@ export async function createFile( @@ -153,7 +153,7 @@ export async function createFile(
await apiPost(`/api/repos/${state.npub}/${state.repo}/file`, {
path: filePath,
content: state.forms.file.content,
message: commitMsg,
commitMessage: commitMsg,
authorName: authorName,
authorEmail: authorEmail,
branch: state.git.currentBranch,
@ -167,8 +167,9 @@ export async function createFile( @@ -167,8 +167,9 @@ export async function createFile(
state.forms.file.content = '';
state.openDialog = null;
// Reload file list
await callbacks.loadFiles(state.files.currentPath);
// Reload file list - use currentPath or empty string for root
const pathToReload = state.files.currentPath || '';
await callbacks.loadFiles(pathToReload);
alert('File created successfully!');
} catch (err) {
@ -233,7 +234,7 @@ export async function deleteFile( @@ -233,7 +234,7 @@ export async function deleteFile(
await apiPost(`/api/repos/${state.npub}/${state.repo}/file`, {
path: filePath,
message: commitMsg,
commitMessage: commitMsg,
authorName: authorName,
authorEmail: authorEmail,
branch: state.git.currentBranch,
@ -247,8 +248,9 @@ export async function deleteFile( @@ -247,8 +248,9 @@ export async function deleteFile(
state.files.currentFile = null;
}
// Reload file list
await callbacks.loadFiles(state.files.currentPath);
// Reload file list - use currentPath or empty string for root
const pathToReload = state.files.currentPath || '';
await callbacks.loadFiles(pathToReload);
alert('File deleted successfully!');
} catch (err) {

6
src/routes/repos/[npub]/[repo]/services/release-operations.ts

@ -26,9 +26,11 @@ export async function loadReleases( @@ -26,9 +26,11 @@ export async function loadReleases(
);
state.releases = data.map((release: any) => ({
id: release.id,
title: release.tags.find((t: string[]) => t[0] === 'title')?.[1],
tagName: release.tags.find((t: string[]) => t[0] === 'tag')?.[1] || '',
tagHash: release.tags.find((t: string[]) => t[0] === 'r' && t[2] === 'tag')?.[1],
releaseNotes: release.content || '',
downloadUrl: release.tags.find((t: string[]) => t[0] === 'r' && t[2] === 'download')?.[1],
isDraft: release.tags.some((t: string[]) => t[0] === 'draft' && t[1] === 'true'),
isPrerelease: release.tags.some((t: string[]) => t[0] === 'prerelease' && t[1] === 'true'),
created_at: release.created_at,
@ -69,17 +71,21 @@ export async function createRelease( @@ -69,17 +71,21 @@ export async function createRelease(
try {
await apiPost(`/api/repos/${state.npub}/${state.repo}/releases`, {
title: state.forms.release.title,
tagName: state.forms.release.tagName,
tagHash: state.forms.release.tagHash,
releaseNotes: state.forms.release.notes,
downloadUrl: state.forms.release.downloadUrl,
isDraft: state.forms.release.isDraft,
isPrerelease: state.forms.release.isPrerelease
});
state.openDialog = null;
state.forms.release.title = '';
state.forms.release.tagName = '';
state.forms.release.tagHash = '';
state.forms.release.notes = '';
state.forms.release.downloadUrl = '';
state.forms.release.isDraft = false;
state.forms.release.isPrerelease = false;
await callbacks.loadReleases();

4
src/routes/repos/[npub]/[repo]/stores/repo-state.ts

@ -95,9 +95,11 @@ export interface PatchFormData { @@ -95,9 +95,11 @@ export interface PatchFormData {
}
export interface ReleaseFormData {
title: string;
tagName: string;
tagHash: string;
notes: string;
downloadUrl: string;
isDraft: boolean;
isPrerelease: boolean;
}
@ -550,7 +552,7 @@ export function createRepoState(): RepoState { @@ -550,7 +552,7 @@ export function createRepoState(): RepoState {
issue: { subject: '', content: '', labels: [''] },
pr: { subject: '', content: '', commitId: '', branchName: '', labels: [''] },
patch: { content: '', subject: '' },
release: { tagName: '', tagHash: '', notes: '', isDraft: false, isPrerelease: false },
release: { title: '', tagName: '', tagHash: '', notes: '', downloadUrl: '', isDraft: false, isPrerelease: false },
discussion: { threadTitle: '', threadContent: '', replyContent: '' },
patchHighlight: { text: '', startLine: 0, endLine: 0, startPos: 0, endPos: 0, comment: '' },
patchComment: { content: '', replyingTo: null },

78
src/routes/repos/[npub]/[repo]/utils/content-renderer.ts

@ -4,6 +4,44 @@ @@ -4,6 +4,44 @@
* Consolidates rendering logic used by DocsViewer, PRsTab, IssuesTab, and PatchesTab
*/
/**
* Extract document title from AsciiDoc content
* @param content - The AsciiDoc content
* @returns The title (text after `= `) or null if not found
*/
export function extractAsciiDocTitle(content: string): string | null {
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('= ')) {
return trimmed.substring(2).trim();
}
}
return null;
}
/**
* Remove document title from AsciiDoc content
* @param content - The AsciiDoc content
* @returns Content without the title line
*/
export function removeAsciiDocTitle(content: string): string {
const lines = content.split('\n');
const result: string[] = [];
let titleRemoved = false;
for (const line of lines) {
const trimmed = line.trim();
if (!titleRemoved && trimmed.startsWith('= ')) {
titleRemoved = true;
continue; // Skip the title line
}
result.push(line);
}
return result.join('\n');
}
/**
* Render content as HTML based on kind or contentType
* @param content - The content to render
@ -28,14 +66,50 @@ export async function renderContent( @@ -28,14 +66,50 @@ export async function renderContent(
if (useAsciiDoc) {
// Use AsciiDoc parser
try {
// Remove document title from content before rendering
// (we'll display it separately in the header)
const contentWithoutTitle = removeAsciiDocTitle(content);
const asciidoctor = (await import('asciidoctor')).default();
const result = asciidoctor.convert(content, {
// Convert with options to get clean HTML body
// standalone: false means no document wrapper
// header_footer: false means no HTML header/footer tags
const result = asciidoctor.convert(contentWithoutTitle, {
safe: 'safe',
standalone: false,
header_footer: false,
doctype: 'article',
attributes: {
'source-highlighter': 'highlight.js'
}
});
return typeof result === 'string' ? result : String(result);
let html = typeof result === 'string' ? result : String(result);
// AsciiDoctor with standalone:false should give us just the body content
// But if there's still a wrapper, extract it
const bodyMatch = html.match(/<body[^>]*>(.*?)<\/body>/s);
if (bodyMatch) {
html = bodyMatch[1];
}
// Don't extract sect1 wrapper - we want all sections, not just the first one
// The sect1 divs are fine, they contain the actual content sections
// Log for debugging
console.log('[ContentRenderer] AsciiDoc converted:', {
inputLength: content.length,
outputLength: html.length,
hasH1: html.includes('<h1'),
hasH2: html.includes('<h2'),
sect1Count: (html.match(/<div[^>]*class="sect1"/g) || []).length,
preview: html.substring(0, 300)
});
return html;
} catch (err) {
console.error('[ContentRenderer] AsciiDoc conversion error:', err);
throw new Error(`Failed to convert AsciiDoc: ${err instanceof Error ? err.message : String(err)}`);
}
} else if (typeof kindOrType === 'string' && kindOrType === 'text') {
// Plain text - escape HTML
return content

Loading…
Cancel
Save