((_, reject) =>
setTimeout(() => reject(new Error('Publish timeout')), 30000)
)
- ]).catch(error => {
+ ]).catch((error: unknown) => {
// Log error but don't throw - we'll mark relays as failed below
- logger.debug({ error, eventId: event.id }, 'Error publishing event to relays');
- return null;
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ logger.debug({ error: errorMessage, eventId: event.id }, 'Error publishing event to relays');
+ return [];
});
- if (publishedRelays) {
+ if (publishedRelays && publishedRelays.length > 0) {
success.push(...publishedRelays);
+ // Mark any relays not in success as failed
+ targetRelays.forEach(relay => {
+ if (!publishedRelays.includes(relay)) {
+ failed.push({ relay, error: 'Relay did not accept event' });
+ }
+ });
} else {
// If publish failed or timed out, mark all as failed
targetRelays.forEach(relay => {
@@ -474,9 +516,10 @@ export class NostrClient {
}
} catch (error) {
// Catch any synchronous errors
- logger.debug({ error, eventId: event.id }, 'Synchronous error in publishEvent');
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ logger.debug({ error: errorMessage, eventId: event.id }, 'Synchronous error in publishEvent');
targetRelays.forEach(relay => {
- failed.push({ relay, error: String(error) });
+ failed.push({ relay, error: errorMessage });
});
}
diff --git a/src/routes/api/repos/[npub]/[repo]/clone/+server.ts b/src/routes/api/repos/[npub]/[repo]/clone/+server.ts
index bb9495d..004abbe 100644
--- a/src/routes/api/repos/[npub]/[repo]/clone/+server.ts
+++ b/src/routes/api/repos/[npub]/[repo]/clone/+server.ts
@@ -105,18 +105,28 @@ export const POST: RequestHandler = async (event) => {
logger.info({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' }, 'Verified unlimited access from proof event');
userLevel = getCachedUserLevel(userPubkeyHex); // Get the cached value
} else {
- // Check if relays are down
- if (verification.relayDown) {
- // Relays are down - check cache again (might have been cached from previous request)
- userLevel = getCachedUserLevel(userPubkeyHex);
- if (!userLevel || !hasUnlimitedAccess(userLevel.level)) {
- logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...', error: verification.error }, 'Relays down and no cached unlimited access');
- throw error(503, 'Relays are temporarily unavailable and no cached access level found. Please verify your access level first by visiting your profile page.');
- }
+ // Verification failed - check cache before denying access
+ // Cache exists for exactly this reason: to allow access when verification temporarily fails
+ userLevel = getCachedUserLevel(userPubkeyHex);
+
+ if (userLevel && hasUnlimitedAccess(userLevel.level)) {
+ // User has cached unlimited access - use it even though verification failed
+ // This handles cases where relays are down or proof event hasn't propagated yet
+ logger.info({
+ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...',
+ error: verification.error,
+ cachedLevel: userLevel.level,
+ cachedAt: new Date(userLevel.cachedAt).toISOString()
+ }, 'Verification failed but using cached unlimited access');
+ } else if (verification.relayDown) {
+ // Relays are down and no cache - temporary issue
+ logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...', error: verification.error }, 'Relays down and no cached unlimited access');
+ throw error(503, 'Relays are temporarily unavailable and no cached access level found. Please verify your access level first by visiting your profile page.');
} else {
- // Verification failed - user doesn't have write access
+ // Verification failed and no cache - user doesn't have write access
logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...', error: verification.error }, 'User does not have unlimited access');
- throw error(403, `Only users with unlimited access can clone repositories to the server. ${verification.error || 'Please verify you can write to at least one default Nostr relay.'}`);
+ const errorMsg = verification.error || 'Please verify you can write to at least one default Nostr relay.';
+ throw error(403, `Only users with unlimited access can clone repositories to the server. ${errorMsg} Note: You only need write access to ONE default relay, not all of them.`);
}
}
} catch (err) {
@@ -132,7 +142,7 @@ export const POST: RequestHandler = async (event) => {
// No proof event or auth header - check if we have any cached level
if (!userLevel) {
logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' }, 'No cached user level and no proof event or NIP-98 auth header');
- throw error(403, 'Only users with unlimited access can clone repositories to the server. Please verify your access level first by visiting your profile page or ensuring you can write to at least one default Nostr relay.');
+ throw error(403, 'Only users with unlimited access can clone repositories to the server. Please verify your access level first by visiting your profile page or ensuring you can write to at least one default Nostr relay. Note: You only need write access to ONE default relay, not all of them.');
}
}
}
@@ -143,7 +153,7 @@ export const POST: RequestHandler = async (event) => {
userPubkeyHex: userPubkeyHex.slice(0, 16) + '...',
cachedLevel: userLevel?.level || 'none'
}, 'User does not have unlimited access');
- throw error(403, 'Only users with unlimited access can clone repositories to the server. Please verify you can write to at least one default Nostr relay.');
+ throw error(403, 'Only users with unlimited access can clone repositories to the server. Please verify you can write to at least one default Nostr relay. Note: You only need write access to ONE default relay, not all of them.');
}
try {
diff --git a/src/routes/api/repos/[npub]/[repo]/verify/+server.ts b/src/routes/api/repos/[npub]/[repo]/verify/+server.ts
index 7e36652..e0fc207 100644
--- a/src/routes/api/repos/[npub]/[repo]/verify/+server.ts
+++ b/src/routes/api/repos/[npub]/[repo]/verify/+server.ts
@@ -183,10 +183,11 @@ export const POST: RequestHandler = createRepoPostHandler(
return error(401, 'Authentication required. Please provide userPubkey.');
}
- // Check if user is a maintainer
+ // Check if user is a maintainer or the repository owner
const isMaintainer = await maintainerService.isMaintainer(userPubkeyHex, context.repoOwnerPubkey, context.repo);
- if (!isMaintainer) {
- return error(403, 'Only repository maintainers can verify clone URLs.');
+ const isOwner = userPubkeyHex === context.repoOwnerPubkey;
+ if (!isMaintainer && !isOwner) {
+ return error(403, 'Only repository owners and maintainers can save announcements.');
}
// Check if repository is cloned
diff --git a/src/routes/repos/+page.svelte b/src/routes/repos/+page.svelte
index 250a139..a93c313 100644
--- a/src/routes/repos/+page.svelte
+++ b/src/routes/repos/+page.svelte
@@ -847,16 +847,6 @@
- {#if userPubkey && canDelete}
-
- {/if}
diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte
index 30a4173..de654ac 100644
--- a/src/routes/repos/[npub]/[repo]/+page.svelte
+++ b/src/routes/repos/[npub]/[repo]/+page.svelte
@@ -192,6 +192,7 @@
generateAnnouncementFileForRepo as generateAnnouncementFileForRepoService,
copyVerificationToClipboard as copyVerificationToClipboardService,
downloadVerificationFile as downloadVerificationFileService,
+ saveAnnouncementToRepo as saveAnnouncementToRepoService,
verifyCloneUrl as verifyCloneUrlService,
deleteAnnouncement as deleteAnnouncementService,
copyEventId as copyEventIdService
@@ -598,13 +599,47 @@
await generateAnnouncementFileForRepoService(state, repoOwnerPubkeyDerived);
}
const copyVerificationToClipboard = () => copyVerificationToClipboardService(state);
+ const downloadVerificationFile = () => downloadVerificationFileService(state);
+ async function saveAnnouncementToRepo() {
+ await saveAnnouncementToRepoService(state, repoOwnerPubkeyDerived);
+ // Reload branches and files to show the new commit
+ if (state.clone.isCloned) {
+ await loadBranches();
+ await loadFiles();
+ }
+ }
async function verifyCloneUrl() {
await verifyCloneUrlService(state, repoOwnerPubkeyDerived, { checkVerification });
}
async function deleteAnnouncement() {
await deleteAnnouncementService(state, repoOwnerPubkeyDerived, announcementEventId);
}
- const downloadVerificationFile = () => downloadVerificationFileService(state);
+
+ async function removeRepoFromServer() {
+ if (!confirm(`Are you sure you want to remove "${state.repo}" from this server?\n\nThis will permanently delete the local clone of the repository. The announcement on Nostr will NOT be deleted.\n\nThis action cannot be undone.\n\nClick OK to delete, or Cancel to abort.`)) {
+ return;
+ }
+
+ try {
+ const headers = buildApiHeaders();
+ const response = await fetch(`/api/repos/${state.npub}/${state.repo}/delete`, {
+ method: 'DELETE',
+ headers
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.message || 'Failed to delete repository');
+ }
+
+ // Redirect to repos list after successful deletion
+ alert('Repository removed from server successfully');
+ goto('/repos');
+ } catch (err) {
+ alert(`Failed to remove repository: ${err instanceof Error ? err.message : String(err)}`);
+ }
+ }
+
const downloadRepository = (ref?: string, filename?: string) => downloadRepoUtil({ npub: state.npub, repo: state.repo, ref, filename });
// Safe wrapper functions for SSR
@@ -613,6 +648,7 @@
const safeToggleBookmark = () => safeAsync(() => toggleBookmark());
const safeForkRepository = () => safeAsync(() => forkRepository());
const safeCloneRepository = () => safeAsync(() => cloneRepository());
+ const safeRemoveRepoFromServer = () => safeAsync(removeRepoFromServer);
const safeHandleBranchChange = (branch: string) => safeSync(() => handleBranchChangeDirect(branch));
// Initialize activeTab from URL query parameter
@@ -855,6 +891,18 @@
await checkVerification();
if (!state.isMounted) return;
+ // Log verification status for maintenance (after check completes)
+ if (state.verification.status) {
+ const status = state.verification.status;
+ console.log('[Page Load] Verification Status:', {
+ verified: status.verified,
+ error: status.error || null,
+ message: status.message || null,
+ cloneCount: status.cloneVerifications?.length || 0,
+ verifiedClones: status.cloneVerifications?.filter(cv => cv.verified).length || 0
+ });
+ }
+
await loadReadme();
if (!state.isMounted) return;
@@ -1055,6 +1103,7 @@
needsClone={needsClone}
allMaintainers={state.maintainers.all}
onCopyEventId={copyEventId}
+ onRemoveFromServer={repoOwnerPubkeyDerived && state.user.pubkeyHex === repoOwnerPubkeyDerived && state.clone.isCloned ? safeRemoveRepoFromServer : undefined}
/>
{/if}
@@ -1905,6 +1954,7 @@
{state}
onCopy={copyVerificationToClipboard}
onDownload={downloadVerificationFile}
+ onSave={state.clone.isCloned && (state.maintainers.isMaintainer || state.user.pubkeyHex === repoOwnerPubkeyDerived) ? saveAnnouncementToRepo : undefined}
onClose={() => state.openDialog = null}
/>
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CloneUrlVerificationDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CloneUrlVerificationDialog.svelte
index 0cec054..6049945 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CloneUrlVerificationDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CloneUrlVerificationDialog.svelte
@@ -73,6 +73,7 @@
.verification-code {
background: var(--bg-secondary, #f5f5f5);
+ color: var(--text-primary);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: monospace;
@@ -112,8 +113,14 @@
}
.primary-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .primary-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.primary-button:disabled {
@@ -122,7 +129,15 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover:not(:disabled) {
+ background: var(--bg-secondary);
}
.cancel-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CommitDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CommitDialog.svelte
index 12ef4b1..3d6a52f 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CommitDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CommitDialog.svelte
@@ -84,12 +84,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateBranchDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateBranchDialog.svelte
index f3307c9..864499b 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateBranchDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateBranchDialog.svelte
@@ -81,12 +81,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateFileDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateFileDialog.svelte
index b63a31c..7f53612 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateFileDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateFileDialog.svelte
@@ -86,12 +86,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateIssueDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateIssueDialog.svelte
index 5ec8ad1..35904a6 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateIssueDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateIssueDialog.svelte
@@ -75,18 +75,26 @@
}
.cancel-button {
- background: var(--cancel-bg, var(--bg-secondary, #2a2a2a));
- color: var(--text-primary, #e0e0e0);
- border: 1px solid var(--border-color, #333);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
}
.cancel-button:hover {
- background: var(--bg-hover, #3a3a3a);
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, var(--accent-color, #2196f3));
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:hover {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePRDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePRDialog.svelte
index bf4ae2d..3538267 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePRDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePRDialog.svelte
@@ -83,18 +83,26 @@
}
.cancel-button {
- background: var(--cancel-bg, var(--bg-secondary, #2a2a2a));
- color: var(--text-primary, #e0e0e0);
- border: 1px solid var(--border-color, #333);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
}
.cancel-button:hover {
- background: var(--bg-hover, #3a3a3a);
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, var(--accent-color, #2196f3));
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:hover {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePatchDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePatchDialog.svelte
index 8ef42a7..caa310b 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePatchDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreatePatchDialog.svelte
@@ -79,18 +79,26 @@
}
.cancel-button {
- background: var(--cancel-bg, var(--bg-secondary, #2a2a2a));
- color: var(--text-primary, #e0e0e0);
- border: 1px solid var(--border-color, #333);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
}
.cancel-button:hover {
- background: var(--bg-hover, #3a3a3a);
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, var(--accent-color, #2196f3));
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:hover {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte
index a7afc8e..c648812 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateReleaseDialog.svelte
@@ -81,12 +81,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateTagDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateTagDialog.svelte
index 4eaa454..3953567 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateTagDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateTagDialog.svelte
@@ -71,12 +71,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateThreadDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateThreadDialog.svelte
index 01f0941..adb742a 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/CreateThreadDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/CreateThreadDialog.svelte
@@ -64,12 +64,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/PatchCommentDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/PatchCommentDialog.svelte
index f23ac7a..864b0f5 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/PatchCommentDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/PatchCommentDialog.svelte
@@ -69,12 +69,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/PatchHighlightDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/PatchHighlightDialog.svelte
index 7ae3b7b..8057a38 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/PatchHighlightDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/PatchHighlightDialog.svelte
@@ -79,12 +79,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/ReplyDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/ReplyDialog.svelte
index cd75ef6..e986ed4 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/ReplyDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/ReplyDialog.svelte
@@ -71,12 +71,26 @@
}
.cancel-button {
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
}
.save-button {
- background: var(--primary-color, #2196f3);
- color: white;
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
}
.save-button:disabled {
diff --git a/src/routes/repos/[npub]/[repo]/components/dialogs/VerificationDialog.svelte b/src/routes/repos/[npub]/[repo]/components/dialogs/VerificationDialog.svelte
index 89be781..9364fcc 100644
--- a/src/routes/repos/[npub]/[repo]/components/dialogs/VerificationDialog.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/dialogs/VerificationDialog.svelte
@@ -7,10 +7,11 @@
state: RepoState;
onCopy: () => void;
onDownload: () => void;
+ onSave?: () => void;
onClose: () => void;
}
- let { open, state, onCopy, onDownload, onClose }: Props = $props();
+ let { open, state, onCopy, onDownload, onSave, onClose }: Props = $props();
@@ -31,7 +32,12 @@
-
+ {#if onSave}
+
+ {/if}
+
@@ -47,6 +53,7 @@
.verification-code {
background: var(--bg-secondary, #f5f5f5);
+ color: var(--text-primary);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: monospace;
@@ -70,6 +77,7 @@
.filename {
font-weight: bold;
font-family: monospace;
+ color: var(--text-primary);
}
.file-actions {
@@ -82,8 +90,17 @@
padding: 0.25rem 0.75rem;
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 4px;
- background: white;
+ background: var(--button-secondary, var(--bg-tertiary));
+ color: var(--text-primary);
cursor: pointer;
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .copy-button:hover,
+ .download-button:hover {
+ background: var(--button-secondary-hover, var(--bg-tertiary));
+ opacity: 0.9;
}
.file-content {
@@ -92,12 +109,15 @@
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
+ background: var(--bg-primary);
+ color: var(--text-primary);
}
.file-content code {
font-family: monospace;
font-size: 0.85rem;
white-space: pre;
+ color: var(--text-primary);
}
.modal-actions {
@@ -108,10 +128,37 @@
}
.cancel-button {
+ padding: 0.5rem 1rem;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ cursor: pointer;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease, color 0.2s ease;
+ }
+
+ .cancel-button:hover {
+ background: var(--bg-secondary);
+ }
+
+ .save-button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
- background: var(--cancel-bg, #e0e0e0);
+ background: var(--button-primary);
+ color: var(--accent-text, #ffffff);
+ font-family: 'IBM Plex Serif', serif;
+ transition: background 0.2s ease;
+ }
+
+ .save-button:hover:not(:disabled) {
+ background: var(--button-primary-hover);
+ }
+
+ .save-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
}
diff --git a/src/routes/repos/[npub]/[repo]/services/repo-operations.ts b/src/routes/repos/[npub]/[repo]/services/repo-operations.ts
index 072e66f..9e9d982 100644
--- a/src/routes/repos/[npub]/[repo]/services/repo-operations.ts
+++ b/src/routes/repos/[npub]/[repo]/services/repo-operations.ts
@@ -13,6 +13,8 @@ import { getUserRelays } from '$lib/services/nostr/user-relays.js';
import { KIND } from '$lib/types/nostr.js';
import type { NostrEvent } from '$lib/types/nostr.js';
import { goto } from '$app/navigation';
+import logger from '$lib/services/logger.js';
+import { isNIP07Available } from '$lib/services/nostr/nip07-signer.js';
interface RepoOperationsCallbacks {
checkCloneStatus: (force: boolean) => Promise;
@@ -110,9 +112,94 @@ export async function cloneRepository(
): Promise {
if (state.clone.cloning) return;
+ if (!state.user.pubkeyHex) {
+ alert('Please log in to clone repositories.');
+ return;
+ }
+
state.clone.cloning = true;
try {
- const data = await apiPost<{ alreadyExists?: boolean }>(`/api/repos/${state.npub}/${state.repo}/clone`, {});
+ // Create and send proof event to verify write access
+ // This ensures the clone endpoint can verify access even if cache is empty
+ let proofEvent: NostrEvent | null = null;
+ try {
+ if (isNIP07Available() && state.user.pubkeyHex) {
+ const { createProofEvent } = await import('$lib/services/nostr/relay-write-proof.js');
+ const { signEventWithNIP07 } = await import('$lib/services/nostr/nip07-signer.js');
+
+ // Create proof event template
+ const proofEventTemplate = createProofEvent(
+ state.user.pubkeyHex,
+ `gitrepublic-clone-proof-${Date.now()}`
+ );
+
+ // Sign with NIP-07
+ proofEvent = await signEventWithNIP07(proofEventTemplate);
+
+ // Publish to relays so server can verify it
+ // User only needs write access to ONE relay, not all
+ const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
+ let publishResult;
+ try {
+ publishResult = await nostrClient.publishEvent(proofEvent, DEFAULT_NOSTR_RELAYS).catch((err: unknown) => {
+ // Catch any unhandled errors from publishEvent
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ logger.debug({ error: errorMessage }, '[Clone] Error in publishEvent, marking all relays as failed');
+ // Return a result with all relays failed
+ return {
+ success: [],
+ failed: DEFAULT_NOSTR_RELAYS.map(relay => ({ relay, error: errorMessage }))
+ };
+ });
+ } catch (err) {
+ // Catch synchronous errors
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ logger.debug({ error: errorMessage }, '[Clone] Synchronous error publishing proof event');
+ publishResult = {
+ success: [],
+ failed: DEFAULT_NOSTR_RELAYS.map(relay => ({ relay, error: errorMessage }))
+ };
+ }
+
+ // If at least one relay accepted the event, wait for propagation
+ // If all relays failed, still try (might be cached or server can retry)
+ if (publishResult && publishResult.success.length > 0) {
+ logger.debug({
+ successCount: publishResult.success.length,
+ successfulRelays: publishResult.success,
+ failedRelays: publishResult.failed.map(f => f.relay)
+ }, '[Clone] Proof event published to at least one relay, waiting for propagation');
+ // Wait longer for event to propagate to relays (3 seconds should be enough)
+ await new Promise(resolve => setTimeout(resolve, 3000));
+ } else {
+ const failedDetails = publishResult?.failed.map(f => `${f.relay}: ${f.error}`) || ['Unknown error'];
+ logger.warn({
+ failedRelays: failedDetails
+ }, '[Clone] Proof event failed to publish to all relays, but continuing (server may retry or use cache)');
+ // Still wait a bit in case some relays are slow
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ }
+
+ // Clean up client
+ try {
+ nostrClient.close();
+ } catch (closeErr) {
+ // Ignore close errors
+ logger.debug({ error: closeErr }, '[Clone] Error closing NostrClient');
+ }
+ }
+ } catch (proofErr) {
+ // If proof creation fails, continue anyway - clone endpoint will check cache
+ logger.debug({ error: proofErr }, '[Clone] Failed to create proof event, will rely on cache');
+ }
+
+ // Send clone request with proof event in body (if available)
+ const requestBody: { proofEvent?: NostrEvent } = {};
+ if (proofEvent) {
+ requestBody.proofEvent = proofEvent;
+ }
+
+ const data = await apiPost<{ alreadyExists?: boolean }>(`/api/repos/${state.npub}/${state.repo}/clone`, requestBody);
if (data.alreadyExists) {
alert('Repository already exists locally.');
@@ -382,7 +469,25 @@ export async function checkVerification(
state.verification.status = { verified: false, error: 'Failed to check verification' };
} finally {
state.loading.verification = false;
- console.log('[Verification] Status after check:', state.verification.status);
+
+ // Log verification status for maintenance
+ if (state.verification.status) {
+ const status = state.verification.status;
+ console.log('[Verification Status]', {
+ verified: status.verified,
+ error: status.error || null,
+ message: status.message || null,
+ cloneCount: status.cloneVerifications?.length || 0,
+ verifiedClones: status.cloneVerifications?.filter(cv => cv.verified).length || 0,
+ cloneDetails: status.cloneVerifications?.map(cv => ({
+ url: cv.url.substring(0, 50) + (cv.url.length > 50 ? '...' : ''),
+ verified: cv.verified,
+ error: cv.error || null
+ })) || []
+ });
+ } else {
+ console.log('[Verification Status] Not available');
+ }
}
}
@@ -593,26 +698,36 @@ export async function generateAnnouncementFileForRepo(
}
try {
- // Fetch the repository announcement event
- const { NostrClient } = await import('$lib/services/nostr/nostr-client.js');
- const { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } = await import('$lib/config.js');
- const { KIND } = await import('$lib/types/nostr.js');
- const nostrClient = new NostrClient([...new Set([...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS])]);
- const events = await nostrClient.fetchEvents([
- {
- kinds: [KIND.REPO_ANNOUNCEMENT],
- authors: [repoOwnerPubkeyDerived],
- '#d': [state.repo],
- limit: 1
+ // First, try to use the announcement from pageData (already loaded)
+ let announcement: NostrEvent | null = null;
+
+ if (state.pageData?.announcement) {
+ announcement = state.pageData.announcement as NostrEvent;
+ }
+
+ // If not available in pageData, fetch from Nostr
+ if (!announcement) {
+ const { NostrClient } = await import('$lib/services/nostr/nostr-client.js');
+ const { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } = await import('$lib/config.js');
+ const { KIND } = await import('$lib/types/nostr.js');
+ const nostrClient = new NostrClient([...new Set([...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS])]);
+ const events = await nostrClient.fetchEvents([
+ {
+ kinds: [KIND.REPO_ANNOUNCEMENT],
+ authors: [repoOwnerPubkeyDerived],
+ '#d': [state.repo],
+ limit: 1
+ }
+ ]);
+
+ if (events.length === 0) {
+ state.error = 'Repository announcement not found. Please ensure the repository is registered on Nostr.';
+ return;
}
- ]);
- if (events.length === 0) {
- state.error = 'Repository announcement not found. Please ensure the repository is registered on Nostr.';
- return;
+ announcement = events[0] as NostrEvent;
}
- const announcement = events[0] as NostrEvent;
// Generate announcement event JSON (for download/reference)
state.verification.fileContent = JSON.stringify(announcement, null, 2) + '\n';
state.openDialog = 'verification';
@@ -636,6 +751,53 @@ export function copyVerificationToClipboard(state: RepoState): void {
});
}
+/**
+ * Save announcement to repository
+ * Uses the existing verify endpoint which saves and commits the announcement
+ */
+export async function saveAnnouncementToRepo(
+ state: RepoState,
+ repoOwnerPubkeyDerived: string | null
+): Promise {
+ if (!repoOwnerPubkeyDerived || !state.user.pubkeyHex) {
+ state.error = 'Unable to save announcement: missing repository or user information';
+ return;
+ }
+
+ if (!state.clone.isCloned) {
+ state.error = 'Repository must be cloned first. Please clone the repository before saving the announcement.';
+ return;
+ }
+
+ // Check if user is owner or maintainer
+ if (!state.maintainers.isMaintainer && state.user.pubkeyHex !== repoOwnerPubkeyDerived) {
+ state.error = 'Only repository owners and maintainers can save announcements.';
+ return;
+ }
+
+ try {
+ state.creating.announcement = true;
+ state.error = null;
+
+ // Use the existing verify endpoint which saves and commits the announcement
+ const data = await apiRequest<{ message?: string; announcementId?: string }>(`/api/repos/${state.npub}/${state.repo}/verify`, {
+ method: 'POST'
+ } as RequestInit);
+
+ // Close dialog and show success
+ state.openDialog = null;
+ alert(data.message || 'Announcement saved to repository successfully!');
+
+ // Reload branches and files to show the new commit
+ // The callbacks will be passed from the component
+ } catch (err) {
+ console.error('Failed to save announcement:', err);
+ state.error = `Failed to save announcement: ${err instanceof Error ? err.message : String(err)}`;
+ } finally {
+ state.creating.announcement = false;
+ }
+}
+
/**
* Download verification file
*/