Browse Source

bug-fixes

Nostr-Signature: e96c955f550a94c9c6d1228d2a7e479ced331334aaa4eea84525b362b8484d6e 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 1218bd9e449404ccc56c5727e8bdff5db31e37c2053a2d91ba02d214c0988173ba480010e53401661cb439884308a575230a7a12124f8e6d8f058c8a804a42f6
main
Silberengel 3 weeks ago
parent
commit
ac06b44de8
  1. 1
      nostr/commit-signatures.jsonl
  2. 26
      src/app.css
  3. 12
      src/lib/components/RepoHeaderEnhanced.svelte
  4. 5
      src/lib/components/TransferNotification.svelte
  5. 162
      src/lib/styles/repo.css
  6. 2
      src/routes/dashboard/+page.svelte
  7. 2
      src/routes/repos/+page.svelte
  8. 196
      src/routes/repos/[npub]/[repo]/+page.svelte
  9. 17
      src/routes/signup/+page.svelte
  10. 2
      src/routes/users/[npub]/+page.svelte
  11. 3
      static/icons/check.svg
  12. 3
      static/icons/chevron-down.svg
  13. 4
      static/icons/search.svg
  14. 5
      static/icons/x-circle.svg

1
nostr/commit-signatures.jsonl

@ -59,3 +59,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771829031,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix file management and refactor"]],"content":"Signed commit: fix file management and refactor","id":"626196cdbf9eab28b44990706281878083d66983b503e8a81df7421054ed6caf","sig":"516c0001a800083411a1e04340e82116a82c975f38b984e92ebe021b61271ba7d6f645466ddba3594320c228193e708675a5d7a144b2f3d5e9bfbc65c4c7372b"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771829031,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix file management and refactor"]],"content":"Signed commit: fix file management and refactor","id":"626196cdbf9eab28b44990706281878083d66983b503e8a81df7421054ed6caf","sig":"516c0001a800083411a1e04340e82116a82c975f38b984e92ebe021b61271ba7d6f645466ddba3594320c228193e708675a5d7a144b2f3d5e9bfbc65c4c7372b"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771836045,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix repo management and refactor\nimplement more GRASP support"]],"content":"Signed commit: fix repo management and refactor\nimplement more GRASP support","id":"6ae016621b13e22809e7bcebe34e5250fd6e0767d2b12ca634104def4ca78a29","sig":"99c34f66a8a67d352622621536545b7dee11cfd9d14a007ec0550d138109116a2f24483c6836fea59b94b9e96066fba548bcb7600bc55adbe0562d999c3c651d"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771836045,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix repo management and refactor\nimplement more GRASP support"]],"content":"Signed commit: fix repo management and refactor\nimplement more GRASP support","id":"6ae016621b13e22809e7bcebe34e5250fd6e0767d2b12ca634104def4ca78a29","sig":"99c34f66a8a67d352622621536545b7dee11cfd9d14a007ec0550d138109116a2f24483c6836fea59b94b9e96066fba548bcb7600bc55adbe0562d999c3c651d"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771838236,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor repo manager"]],"content":"Signed commit: refactor repo manager","id":"d134c35516991f27e47ed8a4aa0d3f1d6e6be41c46c9cf3f6c982c1442b09b4b","sig":"cb699fae6a8e44a3b9123f215749f6fec0470c75a0401a94c37dfb8e572c07281b3941862e704b868663f943c573ab2ee9fec217e87f7be567cc6bb3514cacdb"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771838236,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor repo manager"]],"content":"Signed commit: refactor repo manager","id":"d134c35516991f27e47ed8a4aa0d3f1d6e6be41c46c9cf3f6c982c1442b09b4b","sig":"cb699fae6a8e44a3b9123f215749f6fec0470c75a0401a94c37dfb8e572c07281b3941862e704b868663f943c573ab2ee9fec217e87f7be567cc6bb3514cacdb"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771840654,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"0580e0df8000275817f040bbd6c04dfdfbff08a366df7a1686f227d8b7310053","sig":"9a238266f989c0664dc5c9743675907477e2fcb5311e8edeb505dec97027f619f6dc6742ee5f3887ff6a864274b45005fc7dd4432f8e2772dfe0bb7e2d8a449c"}

26
src/app.css

@ -1214,6 +1214,8 @@ button.theme-option.active img.theme-icon-option,
color: var(--text-primary); color: var(--text-primary);
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: 0.9rem; font-size: 0.9rem;
/* Ensure good contrast in all themes */
min-height: 2.5rem;
} }
.repo-badge:hover { .repo-badge:hover {
@ -1222,6 +1224,7 @@ button.theme-option.active img.theme-icon-option,
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 2px 4px var(--shadow-color-light); box-shadow: 0 2px 4px var(--shadow-color-light);
font-size: 0.9rem; /* Preserve font size on hover */ font-size: 0.9rem; /* Preserve font size on hover */
color: var(--text-primary);
} }
.repo-badge-image { .repo-badge-image {
@ -1230,6 +1233,9 @@ button.theme-option.active img.theme-icon-option,
object-fit: cover; object-fit: cover;
border-radius: 50%; border-radius: 50%;
flex-shrink: 0; flex-shrink: 0;
/* Ensure image is visible in all themes */
border: 1px solid var(--border-color);
background: var(--bg-secondary);
} }
.repo-badge-icon { .repo-badge-icon {
@ -1238,8 +1244,23 @@ button.theme-option.active img.theme-icon-option,
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.2rem;
flex-shrink: 0; flex-shrink: 0;
/* Ensure icon is visible in all themes */
color: var(--text-primary);
}
.repo-badge-icon svg {
width: 100%;
height: 100%;
stroke: var(--text-primary);
fill: none;
}
.repo-badge-icon img {
width: 100%;
height: 100%;
object-fit: contain;
filter: var(--icon-filter, none);
} }
.repo-badge-name { .repo-badge-name {
@ -1248,6 +1269,9 @@ button.theme-option.active img.theme-icon-option,
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 200px; max-width: 200px;
color: var(--text-primary);
/* Ensure text is readable */
line-height: 1.4;
} }
.repo-badge.transferred { .repo-badge.transferred {

12
src/lib/components/RepoHeaderEnhanced.svelte

@ -207,11 +207,17 @@
Create Patch Create Patch
</button> </button>
{/if} {/if}
{#if hasUnlimitedAccess && (isRepoCloned === false || isRepoCloned === null) && onCloneToServer} {#if (isRepoCloned === false || isRepoCloned === null) && onCloneToServer}
<button <button
class="menu-item" class="menu-item"
onclick={() => { onCloneToServer(); showMoreMenu = false; }} onclick={() => {
disabled={cloning || checkingCloneStatus} if (hasUnlimitedAccess) {
onCloneToServer();
}
showMoreMenu = false;
}}
disabled={cloning || checkingCloneStatus || !hasUnlimitedAccess}
title={!hasUnlimitedAccess ? 'Unlimited access required to clone repositories' : undefined}
> >
{cloning ? 'Cloning...' : (checkingCloneStatus ? 'Checking...' : 'Clone to Server')} {cloning ? 'Cloning...' : (checkingCloneStatus ? 'Checking...' : 'Clone to Server')}
</button> </button>

5
src/lib/components/TransferNotification.svelte

@ -73,10 +73,7 @@
<div class="notification-header"> <div class="notification-header">
<h3>Repository Ownership Transfer</h3> <h3>Repository Ownership Transfer</h3>
<button class="close-button" onclick={handleDismiss} aria-label="Dismiss"> <button class="close-button" onclick={handleDismiss} aria-label="Dismiss">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <img src="/icons/x.svg" alt="Close" class="icon-inline" />
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button> </button>
</div> </div>
<div class="notification-body"> <div class="notification-body">

162
src/lib/styles/repo.css

@ -7,6 +7,116 @@
overflow: hidden; overflow: hidden;
} }
.repo-not-cloned-message {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
padding: 2rem;
}
.repo-not-cloned-message .message-content {
max-width: 600px;
text-align: center;
padding: 2rem;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
box-shadow: 0 2px 8px var(--shadow-color-light);
}
.repo-not-cloned-message .message-content h2 {
margin: 0 0 1rem 0;
color: var(--text-primary);
font-size: 1.5rem;
}
.repo-not-cloned-message .message-content p {
margin: 0.75rem 0;
color: var(--text-secondary);
line-height: 1.6;
}
.repo-not-cloned-message .message-content p:first-of-type {
margin-top: 0;
}
.repo-not-cloned-message .message-content p:last-of-type {
margin-bottom: 0;
}
.read-only-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
margin-left: 0.5rem;
font-size: 0.75rem;
font-weight: 500;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0.25rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
vertical-align: middle;
}
.read-only-banner {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 0.75rem 1rem;
margin: 0;
}
.read-only-banner .banner-content {
display: flex;
align-items: center;
gap: 0.75rem;
max-width: 1400px;
margin: 0 auto;
font-size: 0.875rem;
color: var(--text-secondary);
}
.read-only-banner .banner-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
color: var(--text-secondary);
}
.read-only-banner .banner-content span {
flex: 1;
line-height: 1.5;
}
.read-only-banner .banner-content strong {
color: var(--text-primary);
font-weight: 600;
}
.read-only-banner .clone-button-banner {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
background: var(--accent);
color: var(--accent-text, #fff);
border: 1px solid var(--accent);
border-radius: 0.375rem;
cursor: pointer;
transition: opacity 0.2s, background-color 0.2s;
font-weight: 500;
white-space: nowrap;
}
.read-only-banner .clone-button-banner:hover:not(:disabled) {
opacity: 0.9;
background: var(--accent-hover, var(--accent));
}
.read-only-banner .clone-button-banner:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.repo-metadata-section { .repo-metadata-section {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
background: var(--card-bg, #ffffff); background: var(--card-bg, #ffffff);
@ -339,6 +449,39 @@
border: 1px solid var(--error-text); border: 1px solid var(--error-text);
} }
.error .error-message {
margin-bottom: 0.75rem;
}
.error .error-actions {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--error-text);
opacity: 0.7;
}
.error .clone-button-inline {
padding: 0.5rem 1rem;
font-size: 0.875rem;
background: var(--accent);
color: var(--accent-text, #fff);
border: 1px solid var(--accent);
border-radius: 0.375rem;
cursor: pointer;
transition: opacity 0.2s, background-color 0.2s;
font-weight: 500;
}
.error .clone-button-inline:hover:not(:disabled) {
opacity: 0.9;
background: var(--accent-hover, var(--accent));
}
.error .clone-button-inline:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.readme-section { .readme-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1553,17 +1696,23 @@ span.clone-more {
} }
.reachability-refresh-button { .reachability-refresh-button {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
background: var(--bg-secondary, #e8e8e8); background: var(--bg-secondary);
border: 1px solid var(--border-color, #ccc); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: opacity 0.2s; transition: opacity 0.2s, background-color 0.2s, border-color 0.2s;
color: var(--text-primary);
} }
.reachability-refresh-button:hover:not(:disabled) { .reachability-refresh-button:hover:not(:disabled) {
opacity: 0.8; opacity: 0.8;
background: var(--bg-tertiary);
border-color: var(--accent);
} }
.reachability-refresh-button:disabled { .reachability-refresh-button:disabled {
@ -1571,6 +1720,13 @@ span.clone-more {
cursor: not-allowed; cursor: not-allowed;
} }
.reachability-refresh-button .refresh-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-primary);
}
.server-type-badge { .server-type-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

2
src/routes/dashboard/+page.svelte

@ -313,7 +313,7 @@
{item.state} {item.state}
</span> </span>
{#if isPR && 'merged_at' in item && item.merged_at} {#if isPR && 'merged_at' in item && item.merged_at}
<span class="merged-indicator"> Merged</span> <span class="merged-indicator"><img src="/icons/check-circle.svg" alt="Merged" class="icon-inline" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 4px;" /> Merged</span>
{/if} {/if}
</div> </div>
<div class="item-info"> <div class="item-info">

2
src/routes/repos/+page.svelte

@ -381,7 +381,7 @@
} }
async function deleteLocalRepo(npub: string, repo: string) { async function deleteLocalRepo(npub: string, repo: string) {
if (!confirm(` Are you sure you want to delete the local clone of "${repo}"?\n\nThis will permanently remove the repository from this server. The announcement on Nostr will NOT be deleted.\n\nThis action cannot be undone.\n\nClick OK to delete, or Cancel to abort.`)) { if (!confirm(`Are you sure you want to delete the local clone of "${repo}"?\n\nThis will permanently remove the repository from this server. The announcement on Nostr will NOT be deleted.\n\nThis action cannot be undone.\n\nClick OK to delete, or Cancel to abort.`)) {
return; return;
} }

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

@ -248,9 +248,14 @@
} }
} }
let copyingCloneUrl = $state(false); let copyingCloneUrl = $state(false);
let apiFallbackAvailable = $state<boolean | null>(null); // null = unknown, true = API fallback works, false = doesn't work
// Helper: Check if repo needs to be cloned for write operations // Helper: Check if repo needs to be cloned for write operations
const needsClone = $derived(isRepoCloned === false); const needsClone = $derived(isRepoCloned === false);
// Helper: Check if we can use API fallback for read-only operations
const canUseApiFallback = $derived(apiFallbackAvailable === true);
// Helper: Check if we have any way to view the repo (cloned or API fallback)
const canViewRepo = $derived(isRepoCloned === true || canUseApiFallback);
const cloneTooltip = 'Please clone this repo to use this feature.'; const cloneTooltip = 'Please clone this repo to use this feature.';
// Copy clone URL to clipboard // Copy clone URL to clipboard
@ -330,16 +335,39 @@
// Tabs menu - defined after issues and prs // Tabs menu - defined after issues and prs
// Order: Files, Issues, PRs, Patches, Discussion, History, Tags, Docs // Order: Files, Issues, PRs, Patches, Discussion, History, Tags, Docs
const tabs = $derived([ // Show tabs that require cloned repo when repo is cloned OR API fallback is available
{ id: 'files', label: 'Files', icon: '/icons/file-text.svg' }, const tabs = $derived.by(() => {
{ id: 'issues', label: 'Issues', icon: '/icons/alert-circle.svg' }, const allTabs = [
{ id: 'prs', label: 'Pull Requests', icon: '/icons/git-pull-request.svg' }, { id: 'files', label: 'Files', icon: '/icons/file-text.svg', requiresClone: true },
{ id: 'patches', label: 'Patches', icon: '/icons/clipboard-list.svg' }, { id: 'issues', label: 'Issues', icon: '/icons/alert-circle.svg', requiresClone: false },
{ id: 'discussions', label: 'Discussions', icon: '/icons/message-circle.svg' }, { id: 'prs', label: 'Pull Requests', icon: '/icons/git-pull-request.svg', requiresClone: false },
{ id: 'history', label: 'Commit History', icon: '/icons/git-commit.svg' }, { id: 'patches', label: 'Patches', icon: '/icons/clipboard-list.svg', requiresClone: false },
{ id: 'tags', label: 'Tags', icon: '/icons/tag.svg' }, { id: 'discussions', label: 'Discussions', icon: '/icons/message-circle.svg', requiresClone: false },
{ id: 'docs', label: 'Docs', icon: '/icons/book.svg' } { id: 'history', label: 'Commit History', icon: '/icons/git-commit.svg', requiresClone: true },
]); { id: 'tags', label: 'Tags', icon: '/icons/tag.svg', requiresClone: true },
{ id: 'docs', label: 'Docs', icon: '/icons/book.svg', requiresClone: false }
];
// Show all tabs if repo is cloned OR API fallback is available
// Otherwise, only show tabs that don't require cloning
if (isRepoCloned === false && !canUseApiFallback) {
return allTabs.filter(tab => !tab.requiresClone).map(({ requiresClone, ...tab }) => tab);
}
// Return all tabs when repo is cloned, API fallback is available, or status is unknown (remove requiresClone property)
return allTabs.map(({ requiresClone, ...tab }) => tab);
});
// Redirect to a valid tab if current tab requires cloning but repo isn't cloned and API fallback isn't available
$effect(() => {
if (isRepoCloned === false && !canUseApiFallback && tabs.length > 0) {
const currentTab = tabs.find(t => t.id === activeTab);
if (!currentTab) {
// Current tab requires cloning, switch to first available tab
activeTab = tabs[0].id as typeof activeTab;
}
}
});
// Patches // Patches
let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number }>>([]); let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number }>>([]);
@ -822,14 +850,15 @@
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const newMap = new Map<string, { reachable: boolean; error?: string; checkedAt: number }>(); const newMap = new Map<string, { reachable: boolean; error?: string; checkedAt: number; serverType: 'git' | 'grasp' | 'unknown' }>();
if (data.results && Array.isArray(data.results)) { if (data.results && Array.isArray(data.results)) {
for (const result of data.results) { for (const result of data.results) {
newMap.set(result.url, { newMap.set(result.url, {
reachable: result.reachable, reachable: result.reachable,
error: result.error, error: result.error,
checkedAt: result.checkedAt checkedAt: result.checkedAt,
serverType: result.serverType || 'unknown'
}); });
} }
} }
@ -1386,11 +1415,27 @@
// If response is 200, repo exists and is accessible (cloned) // If response is 200, repo exists and is accessible (cloned)
const wasCloned = response.status !== 404; const wasCloned = response.status !== 404;
isRepoCloned = wasCloned; isRepoCloned = wasCloned;
console.log(`[Clone Status] Repo ${wasCloned ? 'is cloned' : 'is not cloned'} (status: ${response.status})`);
// If repo is not cloned, check if API fallback is available
if (!wasCloned) {
// Try to detect API fallback by checking if we have clone URLs
if (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) {
// We have clone URLs, so API fallback might work - will be detected when loadBranches() runs
apiFallbackAvailable = null; // Will be set to true if a subsequent request succeeds
} else {
apiFallbackAvailable = false;
}
} else {
// Repo is cloned, API fallback not needed
apiFallbackAvailable = false;
}
console.log(`[Clone Status] Repo ${wasCloned ? 'is cloned' : 'is not cloned'} (status: ${response.status}), API fallback: ${apiFallbackAvailable}`);
} catch (err) { } catch (err) {
// On error, assume not cloned // On error, assume not cloned
console.warn('[Clone Status] Error checking clone status:', err); console.warn('[Clone Status] Error checking clone status:', err);
isRepoCloned = false; isRepoCloned = false;
apiFallbackAvailable = false;
} finally { } finally {
checkingCloneStatus = false; checkingCloneStatus = false;
} }
@ -1424,6 +1469,8 @@
alert('Repository cloned successfully! The repository is now available on this server.'); alert('Repository cloned successfully! The repository is now available on this server.');
// Force refresh clone status // Force refresh clone status
await checkCloneStatus(true); await checkCloneStatus(true);
// Reset API fallback status since repo is now cloned
apiFallbackAvailable = false;
// Reload data to use the cloned repo instead of API // Reload data to use the cloned repo instead of API
await Promise.all([ await Promise.all([
loadBranches(), loadBranches(),
@ -1475,7 +1522,7 @@
console.log(`[Fork UI] - Announcement ID: ${data.fork.announcementId}`); console.log(`[Fork UI] - Announcement ID: ${data.fork.announcementId}`);
console.log(`[Fork UI] - Ownership Transfer ID: ${data.fork.ownershipTransferId}`); console.log(`[Fork UI] - Ownership Transfer ID: ${data.fork.ownershipTransferId}`);
alert(`${message}\n\nRedirecting to your fork...`); alert(`${message}\n\nRedirecting to your fork...`);
goto(`/repos/${data.fork.npub}/${data.fork.repo}`); goto(`/repos/${data.fork.npub}/${data.fork.repo}`);
} else { } else {
const errorMessage = data.error || 'Failed to fork repository'; const errorMessage = data.error || 'Failed to fork repository';
@ -1491,13 +1538,13 @@
} }
error = fullError; error = fullError;
alert(`Fork failed!\n\n${fullError}`); alert(`Fork failed!\n\n${fullError}`);
} }
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fork repository'; const errorMessage = err instanceof Error ? err.message : 'Failed to fork repository';
console.error(`[Fork UI] ✗ Unexpected error: ${errorMessage}`, err); console.error(`[Fork UI] ✗ Unexpected error: ${errorMessage}`, err);
error = errorMessage; error = errorMessage;
alert(`Fork failed!\n\n${errorMessage}`); alert(`Fork failed!\n\n${errorMessage}`);
} finally { } finally {
forking = false; forking = false;
} }
@ -2610,12 +2657,12 @@
} }
// First confirmation // First confirmation
if (!confirm(' WARNING: Are you sure you want to delete this repository announcement?\n\nThis will permanently delete the repository announcement from Nostr relays. This action CANNOT be undone.\n\nClick OK to continue, or Cancel to abort.')) { if (!confirm('WARNING: Are you sure you want to delete this repository announcement?\n\nThis will permanently delete the repository announcement from Nostr relays. This action CANNOT be undone.\n\nClick OK to continue, or Cancel to abort.')) {
return; return;
} }
// Second confirmation for critical operation // Second confirmation for critical operation
if (!confirm(' FINAL CONFIRMATION: This will permanently delete the repository announcement.\n\nAre you absolutely certain you want to proceed?\n\nThis action CANNOT be undone.')) { if (!confirm('FINAL CONFIRMATION: This will permanently delete the repository announcement.\n\nAre you absolutely certain you want to proceed?\n\nThis action CANNOT be undone.')) {
return; return;
} }
@ -2714,6 +2761,11 @@
}); });
if (response.ok) { if (response.ok) {
branches = await response.json(); branches = await response.json();
// If repo is not cloned but we got branches, API fallback is available
if (isRepoCloned === false && branches.length > 0) {
apiFallbackAvailable = true;
}
if (branches.length > 0) { if (branches.length > 0) {
// Branches can be an array of objects with .name property or array of strings // Branches can be an array of objects with .name property or array of strings
const branchNames = branches.map((b: any) => typeof b === 'string' ? b : b.name); const branchNames = branches.map((b: any) => typeof b === 'string' ? b : b.name);
@ -2762,9 +2814,24 @@
} }
} }
} else if (response.status === 404) { } else if (response.status === 404) {
// Check if this is a "not cloned" error with API fallback suggestion
const errorText = await response.text().catch(() => '');
if (errorText.includes('not cloned locally') && errorText.includes('API')) {
// API fallback might be available, but this specific request failed
// Try to detect if API fallback works by checking if we have clone URLs
if (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) {
// We have clone URLs, so API fallback might work - mark as unknown for now
// It will be set to true if a subsequent request succeeds
apiFallbackAvailable = null;
} else {
apiFallbackAvailable = false;
}
} else {
// Repository not provisioned yet - set error message and flag // Repository not provisioned yet - set error message and flag
repoNotFound = true; repoNotFound = true;
error = `Repository not found. This repository exists in Nostr but hasn't been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.`; error = `Repository not found. This repository exists in Nostr but hasn't been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.`;
apiFallbackAvailable = false;
}
} else if (response.status === 403) { } else if (response.status === 403) {
// Access denied - don't set repoNotFound, allow retry after login // Access denied - don't set repoNotFound, allow retry after login
const errorText = await response.text().catch(() => response.statusText); const errorText = await response.text().catch(() => response.statusText);
@ -2807,7 +2874,19 @@
if (!response.ok) { if (!response.ok) {
if (response.status === 404) { if (response.status === 404) {
// Check if this is a "not cloned" error with API fallback suggestion
const errorText = await response.text().catch(() => '');
if (errorText.includes('not cloned locally') && errorText.includes('API')) {
// API fallback might be available, but this specific request failed
if (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) {
apiFallbackAvailable = null; // Unknown, will be set if a request succeeds
} else {
apiFallbackAvailable = false;
}
} else {
repoNotFound = true; repoNotFound = true;
apiFallbackAvailable = false;
}
throw new Error(`Repository not found. This repository exists in Nostr but hasn't been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.`); throw new Error(`Repository not found. This repository exists in Nostr but hasn't been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.`);
} else if (response.status === 403) { } else if (response.status === 403) {
// 403 means access denied - don't set repoNotFound, just show error // 403 means access denied - don't set repoNotFound, just show error
@ -2823,6 +2902,11 @@
files = await response.json(); files = await response.json();
currentPath = path; currentPath = path;
// If repo is not cloned but we got files, API fallback is available
if (isRepoCloned === false && files.length > 0) {
apiFallbackAvailable = true;
}
// Auto-load README if we're in the root directory and no file is currently selected // Auto-load README if we're in the root directory and no file is currently selected
// Only attempt once per path to prevent loops // Only attempt once per path to prevent loops
if (path === '' && !currentFile && !readmeAutoLoadAttempted) { if (path === '' && !currentFile && !readmeAutoLoadAttempted) {
@ -3613,7 +3697,7 @@
} }
async function deleteFile(filePath: string) { async function deleteFile(filePath: string) {
if (!confirm(` Are you sure you want to delete "${filePath}"?\n\nThis will permanently delete the file from the repository. This action cannot be undone.\n\nClick OK to delete, or Cancel to abort.`)) { if (!confirm(`Are you sure you want to delete "${filePath}"?\n\nThis will permanently delete the file from the repository. This action cannot be undone.\n\nClick OK to delete, or Cancel to abort.`)) {
return; return;
} }
@ -3741,7 +3825,7 @@
} }
async function deleteBranch(branchName: string) { async function deleteBranch(branchName: string) {
if (!confirm(` Are you sure you want to delete the branch "${branchName}"?\n\nThis will permanently delete the branch from the repository. This action CANNOT be undone.\n\nClick OK to delete, or Cancel to abort.`)) { if (!confirm(`Are you sure you want to delete the branch "${branchName}"?\n\nThis will permanently delete the branch from the repository. This action CANNOT be undone.\n\nClick OK to delete, or Cancel to abort.`)) {
return; return;
} }
@ -4393,18 +4477,20 @@
aria-expanded={cloneUrlsExpanded} aria-expanded={cloneUrlsExpanded}
> >
<span class="clone-label">Clone URLs:</span> <span class="clone-label">Clone URLs:</span>
<svg class="clone-toggle-icon" class:expanded={cloneUrlsExpanded} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <img src="/icons/chevron-down.svg" alt="" class="clone-toggle-icon icon-inline" class:expanded={cloneUrlsExpanded} />
<path d="M6 9l6 6 6-6"/>
</svg>
</button> </button>
<button <button
class="reachability-refresh-button" class="reachability-refresh-button"
onclick={() => loadCloneUrlReachability(true)} onclick={() => loadCloneUrlReachability(true)}
disabled={loadingReachability} disabled={loadingReachability}
title="Refresh reachability status" title="Refresh reachability status"
style="padding: 0.25rem 0.5rem; font-size: 0.875rem; background: var(--bg-secondary, #e8e8e8); border: 1px solid var(--border-color, #ccc); border-radius: 4px; cursor: pointer;"
> >
{loadingReachability ? 'Checking...' : '🔄 Check Reachability'} {#if loadingReachability}
Checking...
{:else}
<img src="/icons/refresh-cw.svg" alt="" class="refresh-icon icon-inline" />
<span>Check Reachability</span>
{/if}
</button> </button>
</div> </div>
<div class="clone-url-list" class:collapsed={!cloneUrlsExpanded}> <div class="clone-url-list" class:collapsed={!cloneUrlsExpanded}>
@ -4505,17 +4591,60 @@
{/if} {/if}
<main class="repo-view"> <main class="repo-view">
{#if isRepoCloned === false && canUseApiFallback}
<div class="read-only-banner">
<div class="banner-content">
<img src="/icons/alert-circle.svg" alt="Info" class="banner-icon" />
<span>This repository is displayed in <strong>read-only mode</strong> using data from external clone URLs. To enable editing and full features, clone this repository to the server.</span>
{#if hasUnlimitedAccess($userStore.userLevel)}
<button
class="clone-button-banner"
onclick={cloneRepository}
disabled={cloning || checkingCloneStatus}
>
{cloning ? 'Cloning...' : (checkingCloneStatus ? 'Checking...' : 'Clone to Server')}
</button>
{/if}
</div>
</div>
{/if}
{#if error} {#if error}
<div class="error"> <div class="error">
Error: {error} <div class="error-message">
<strong>Error:</strong> {error}
</div>
{#if error.includes('not cloned locally') && hasUnlimitedAccess($userStore.userLevel)}
<div class="error-actions">
<button
class="clone-button-inline"
onclick={cloneRepository}
disabled={cloning || checkingCloneStatus}
>
{cloning ? 'Cloning...' : (checkingCloneStatus ? 'Checking...' : 'Clone to Server')}
</button>
</div>
{/if}
</div> </div>
{/if} {/if}
<!-- Tabs --> <!-- Tabs -->
{#if isRepoCloned === false && !canUseApiFallback && tabs.length === 0}
<div class="repo-not-cloned-message">
<div class="message-content">
<h2>Repository Not Cloned</h2>
<p>This repository has not been cloned to the server yet, and read-only access via external clone URLs is not available.</p>
{#if hasUnlimitedAccess($userStore.userLevel)}
<p>Use the "Clone to Server" option in the repository menu to clone this repository.</p>
{:else}
<p>Contact a server administrator with unlimited access to clone this repository.</p>
{/if}
</div>
</div>
{:else}
<div class="repo-layout"> <div class="repo-layout">
<!-- File Tree Sidebar --> <!-- File Tree Sidebar -->
{#if activeTab === 'files'} {#if activeTab === 'files' && canViewRepo}
<aside class="file-tree" class:hide-on-mobile={!showFileListOnMobile && activeTab === 'files'}> <aside class="file-tree" class:hide-on-mobile={!showFileListOnMobile && activeTab === 'files'}>
<div class="file-tree-header"> <div class="file-tree-header">
<TabsMenu <TabsMenu
@ -4523,7 +4652,7 @@
{tabs} {tabs}
onTabChange={(tab) => activeTab = tab as typeof activeTab} onTabChange={(tab) => activeTab = tab as typeof activeTab}
/> />
<h2>Files</h2> <h2>Files {#if isRepoCloned === false && canUseApiFallback}<span class="read-only-badge">Read-Only</span>{/if}</h2>
<button <button
onclick={toggleWordWrap} onclick={toggleWordWrap}
class="word-wrap-button" class="word-wrap-button"
@ -4596,7 +4725,7 @@
{/if} {/if}
<!-- Commit History View --> <!-- Commit History View -->
{#if activeTab === 'history'} {#if activeTab === 'history' && canViewRepo}
<aside class="history-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'history'}> <aside class="history-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'history'}>
<div class="history-header"> <div class="history-header">
<TabsMenu <TabsMenu
@ -4604,7 +4733,7 @@
{tabs} {tabs}
onTabChange={(tab) => activeTab = tab as typeof activeTab} onTabChange={(tab) => activeTab = tab as typeof activeTab}
/> />
<h2>Commits</h2> <h2>Commits {#if isRepoCloned === false && canUseApiFallback}<span class="read-only-badge">Read-Only</span>{/if}</h2>
<button <button
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile}
class="mobile-toggle-button" class="mobile-toggle-button"
@ -4638,7 +4767,7 @@
{/if} {/if}
<!-- Tags View --> <!-- Tags View -->
{#if activeTab === 'tags'} {#if activeTab === 'tags' && canViewRepo}
<aside class="tags-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'tags'}> <aside class="tags-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'tags'}>
<div class="tags-header"> <div class="tags-header">
<TabsMenu <TabsMenu
@ -4646,7 +4775,7 @@
{tabs} {tabs}
onTabChange={(tab) => activeTab = tab as typeof activeTab} onTabChange={(tab) => activeTab = tab as typeof activeTab}
/> />
<h2>Tags</h2> <h2>Tags {#if isRepoCloned === false && canUseApiFallback}<span class="read-only-badge">Read-Only</span>{/if}</h2>
{#if userPubkey && isMaintainer} {#if userPubkey && isMaintainer}
<button <button
onclick={() => { onclick={() => {
@ -5079,7 +5208,7 @@
content={editedContent} content={editedContent}
language={fileLanguage} language={fileLanguage}
onChange={handleContentChange} onChange={handleContentChange}
readOnly={needsClone} readOnly={needsClone || (isRepoCloned === false && canUseApiFallback)}
/> />
{:else} {:else}
<div class="read-only-editor" class:word-wrap={wordWrap}> <div class="read-only-editor" class:word-wrap={wordWrap}>
@ -5590,6 +5719,7 @@
{/if} {/if}
</div> </div>
</div> </div>
{/if}
</main> </main>
<!-- Create File Dialog --> <!-- Create File Dialog -->

17
src/routes/signup/+page.svelte

@ -2221,10 +2221,7 @@
{#if lookupLoading['repo-existingRepoRef']} {#if lookupLoading['repo-existingRepoRef']}
<span class="loading-text">Loading...</span> <span class="loading-text">Loading...</span>
{:else} {:else}
<svg class="icon-small" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <img src="/icons/search.svg" alt="Search" class="icon-small" />
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
{/if} {/if}
</button> </button>
<button <button
@ -2455,10 +2452,7 @@
{#if lookupLoading[`npub-maintainers-${index}`]} {#if lookupLoading[`npub-maintainers-${index}`]}
<span class="loading-text">Loading...</span> <span class="loading-text">Loading...</span>
{:else} {:else}
<svg class="icon-small" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <img src="/icons/search.svg" alt="Search" class="icon-small" />
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
{/if} {/if}
</button> </button>
{#if maintainers.length > 1} {#if maintainers.length > 1}
@ -2733,7 +2727,7 @@
{/if} {/if}
{#if lookupResults[`doc-${index}`]} {#if lookupResults[`doc-${index}`]}
<div class="lookup-results"> <div class="lookup-results">
<small> Documentation address converted successfully</small> <small><img src="/icons/check.svg" alt="Success" class="icon-inline" style="width: 14px; height: 14px; vertical-align: middle; margin-right: 4px;" /> Documentation address converted successfully</small>
</div> </div>
{/if} {/if}
{/each} {/each}
@ -2789,10 +2783,7 @@
{#if lookupLoading['repo-forkOriginalRepo']} {#if lookupLoading['repo-forkOriginalRepo']}
<span class="loading-text">Loading...</span> <span class="loading-text">Loading...</span>
{:else} {:else}
<svg class="icon-small" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <img src="/icons/search.svg" alt="Search" class="icon-small" />
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
{/if} {/if}
</button> </button>
</div> </div>

2
src/routes/users/[npub]/+page.svelte

@ -1664,7 +1664,7 @@ i *
<div class="access-level-indicator"> <div class="access-level-indicator">
{#if hasUnlimitedAccess(userLevel)} {#if hasUnlimitedAccess(userLevel)}
<span class="access-badge access-unlimited" title="You have unlimited access - you can clone repositories and create new ones"> <span class="access-badge access-unlimited" title="You have unlimited access - you can clone repositories and create new ones">
Unlimited Access <img src="/icons/check-circle.svg" alt="Unlimited Access" class="icon-inline" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 4px;" /> Unlimited Access
</span> </span>
{:else if userLevel === 'rate_limited'} {:else if userLevel === 'rate_limited'}
<span class="access-badge access-limited" title="You have rate-limited access - you can view repositories but cannot clone or create new ones"> <span class="access-badge access-limited" title="You have rate-limited access - you can view repositories but cannot clone or create new ones">

3
static/icons/check.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5"/>
</svg>

After

Width:  |  Height:  |  Size: 219 B

3
static/icons/chevron-down.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

4
static/icons/search.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

5
static/icons/x-circle.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="m15 9-6 6"/>
<path d="m9 9 6 6"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

Loading…
Cancel
Save