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

12
src/lib/components/RepoHeaderEnhanced.svelte

@ -207,11 +207,17 @@ @@ -207,11 +207,17 @@
Create Patch
</button>
{/if}
{#if hasUnlimitedAccess && (isRepoCloned === false || isRepoCloned === null) && onCloneToServer}
{#if (isRepoCloned === false || isRepoCloned === null) && onCloneToServer}
<button
class="menu-item"
onclick={() => { onCloneToServer(); showMoreMenu = false; }}
disabled={cloning || checkingCloneStatus}
onclick={() => {
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')}
</button>

5
src/lib/components/TransferNotification.svelte

@ -73,10 +73,7 @@ @@ -73,10 +73,7 @@
<div class="notification-header">
<h3>Repository Ownership Transfer</h3>
<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">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
<img src="/icons/x.svg" alt="Close" class="icon-inline" />
</button>
</div>
<div class="notification-body">

162
src/lib/styles/repo.css

@ -7,6 +7,116 @@ @@ -7,6 +7,116 @@
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 {
padding: 0.75rem 1rem;
background: var(--card-bg, #ffffff);
@ -339,6 +449,39 @@ @@ -339,6 +449,39 @@
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 {
display: flex;
flex-direction: column;
@ -1553,17 +1696,23 @@ span.clone-more { @@ -1553,17 +1696,23 @@ span.clone-more {
}
.reachability-refresh-button {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
background: var(--bg-secondary, #e8e8e8);
border: 1px solid var(--border-color, #ccc);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
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) {
opacity: 0.8;
background: var(--bg-tertiary);
border-color: var(--accent);
}
.reachability-refresh-button:disabled {
@ -1571,6 +1720,13 @@ span.clone-more { @@ -1571,6 +1720,13 @@ span.clone-more {
cursor: not-allowed;
}
.reachability-refresh-button .refresh-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-primary);
}
.server-type-badge {
display: inline-flex;
align-items: center;

2
src/routes/dashboard/+page.svelte

@ -313,7 +313,7 @@ @@ -313,7 +313,7 @@
{item.state}
</span>
{#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}
</div>
<div class="item-info">

2
src/routes/repos/+page.svelte

@ -381,7 +381,7 @@ @@ -381,7 +381,7 @@
}
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;
}

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

@ -248,9 +248,14 @@ @@ -248,9 +248,14 @@
}
}
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
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.';
// Copy clone URL to clipboard
@ -330,16 +335,39 @@ @@ -330,16 +335,39 @@
// Tabs menu - defined after issues and prs
// Order: Files, Issues, PRs, Patches, Discussion, History, Tags, Docs
const tabs = $derived([
{ id: 'files', label: 'Files', icon: '/icons/file-text.svg' },
{ id: 'issues', label: 'Issues', icon: '/icons/alert-circle.svg' },
{ id: 'prs', label: 'Pull Requests', icon: '/icons/git-pull-request.svg' },
{ id: 'patches', label: 'Patches', icon: '/icons/clipboard-list.svg' },
{ id: 'discussions', label: 'Discussions', icon: '/icons/message-circle.svg' },
{ id: 'history', label: 'Commit History', icon: '/icons/git-commit.svg' },
{ id: 'tags', label: 'Tags', icon: '/icons/tag.svg' },
{ id: 'docs', label: 'Docs', icon: '/icons/book.svg' }
]);
// Show tabs that require cloned repo when repo is cloned OR API fallback is available
const tabs = $derived.by(() => {
const allTabs = [
{ id: 'files', label: 'Files', icon: '/icons/file-text.svg', requiresClone: true },
{ id: 'issues', label: 'Issues', icon: '/icons/alert-circle.svg', requiresClone: false },
{ id: 'prs', label: 'Pull Requests', icon: '/icons/git-pull-request.svg', requiresClone: false },
{ id: 'patches', label: 'Patches', icon: '/icons/clipboard-list.svg', requiresClone: false },
{ id: 'discussions', label: 'Discussions', icon: '/icons/message-circle.svg', requiresClone: false },
{ 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
let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number }>>([]);
@ -822,14 +850,15 @@ @@ -822,14 +850,15 @@
if (response.ok) {
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)) {
for (const result of data.results) {
newMap.set(result.url, {
reachable: result.reachable,
error: result.error,
checkedAt: result.checkedAt
checkedAt: result.checkedAt,
serverType: result.serverType || 'unknown'
});
}
}
@ -1386,11 +1415,27 @@ @@ -1386,11 +1415,27 @@
// If response is 200, repo exists and is accessible (cloned)
const wasCloned = response.status !== 404;
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) {
// On error, assume not cloned
console.warn('[Clone Status] Error checking clone status:', err);
isRepoCloned = false;
apiFallbackAvailable = false;
} finally {
checkingCloneStatus = false;
}
@ -1424,6 +1469,8 @@ @@ -1424,6 +1469,8 @@
alert('Repository cloned successfully! The repository is now available on this server.');
// Force refresh clone status
await checkCloneStatus(true);
// Reset API fallback status since repo is now cloned
apiFallbackAvailable = false;
// Reload data to use the cloned repo instead of API
await Promise.all([
loadBranches(),
@ -1475,7 +1522,7 @@ @@ -1475,7 +1522,7 @@
console.log(`[Fork UI] - Announcement ID: ${data.fork.announcementId}`);
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}`);
} else {
const errorMessage = data.error || 'Failed to fork repository';
@ -1491,13 +1538,13 @@ @@ -1491,13 +1538,13 @@
}
error = fullError;
alert(`Fork failed!\n\n${fullError}`);
alert(`Fork failed!\n\n${fullError}`);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fork repository';
console.error(`[Fork UI] ✗ Unexpected error: ${errorMessage}`, err);
error = errorMessage;
alert(`Fork failed!\n\n${errorMessage}`);
alert(`Fork failed!\n\n${errorMessage}`);
} finally {
forking = false;
}
@ -2610,12 +2657,12 @@ @@ -2610,12 +2657,12 @@
}
// 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;
}
// 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;
}
@ -2714,6 +2761,11 @@ @@ -2714,6 +2761,11 @@
});
if (response.ok) {
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) {
// 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);
@ -2762,9 +2814,24 @@ @@ -2762,9 +2814,24 @@
}
}
} else if (response.status === 404) {
// Repository not provisioned yet - set error message and flag
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.`;
// 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
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.`;
apiFallbackAvailable = false;
}
} else if (response.status === 403) {
// Access denied - don't set repoNotFound, allow retry after login
const errorText = await response.text().catch(() => response.statusText);
@ -2807,7 +2874,19 @@ @@ -2807,7 +2874,19 @@
if (!response.ok) {
if (response.status === 404) {
repoNotFound = true;
// 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;
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.`);
} else if (response.status === 403) {
// 403 means access denied - don't set repoNotFound, just show error
@ -2823,6 +2902,11 @@ @@ -2823,6 +2902,11 @@
files = await response.json();
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
// Only attempt once per path to prevent loops
if (path === '' && !currentFile && !readmeAutoLoadAttempted) {
@ -3613,7 +3697,7 @@ @@ -3613,7 +3697,7 @@
}
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;
}
@ -3741,7 +3825,7 @@ @@ -3741,7 +3825,7 @@
}
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;
}
@ -4393,18 +4477,20 @@ @@ -4393,18 +4477,20 @@
aria-expanded={cloneUrlsExpanded}
>
<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">
<path d="M6 9l6 6 6-6"/>
</svg>
<img src="/icons/chevron-down.svg" alt="" class="clone-toggle-icon icon-inline" class:expanded={cloneUrlsExpanded} />
</button>
<button
class="reachability-refresh-button"
onclick={() => loadCloneUrlReachability(true)}
disabled={loadingReachability}
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>
</div>
<div class="clone-url-list" class:collapsed={!cloneUrlsExpanded}>
@ -4505,17 +4591,60 @@ @@ -4505,17 +4591,60 @@
{/if}
<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}
<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>
{/if}
<!-- 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">
<!-- File Tree Sidebar -->
{#if activeTab === 'files'}
{#if activeTab === 'files' && canViewRepo}
<aside class="file-tree" class:hide-on-mobile={!showFileListOnMobile && activeTab === 'files'}>
<div class="file-tree-header">
<TabsMenu
@ -4523,7 +4652,7 @@ @@ -4523,7 +4652,7 @@
{tabs}
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
onclick={toggleWordWrap}
class="word-wrap-button"
@ -4596,7 +4725,7 @@ @@ -4596,7 +4725,7 @@
{/if}
<!-- Commit History View -->
{#if activeTab === 'history'}
{#if activeTab === 'history' && canViewRepo}
<aside class="history-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'history'}>
<div class="history-header">
<TabsMenu
@ -4604,7 +4733,7 @@ @@ -4604,7 +4733,7 @@
{tabs}
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
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile}
class="mobile-toggle-button"
@ -4638,7 +4767,7 @@ @@ -4638,7 +4767,7 @@
{/if}
<!-- Tags View -->
{#if activeTab === 'tags'}
{#if activeTab === 'tags' && canViewRepo}
<aside class="tags-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'tags'}>
<div class="tags-header">
<TabsMenu
@ -4646,7 +4775,7 @@ @@ -4646,7 +4775,7 @@
{tabs}
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}
<button
onclick={() => {
@ -5079,7 +5208,7 @@ @@ -5079,7 +5208,7 @@
content={editedContent}
language={fileLanguage}
onChange={handleContentChange}
readOnly={needsClone}
readOnly={needsClone || (isRepoCloned === false && canUseApiFallback)}
/>
{:else}
<div class="read-only-editor" class:word-wrap={wordWrap}>
@ -5590,6 +5719,7 @@ @@ -5590,6 +5719,7 @@
{/if}
</div>
</div>
{/if}
</main>
<!-- Create File Dialog -->

17
src/routes/signup/+page.svelte

@ -2221,10 +2221,7 @@ @@ -2221,10 +2221,7 @@
{#if lookupLoading['repo-existingRepoRef']}
<span class="loading-text">Loading...</span>
{: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">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<img src="/icons/search.svg" alt="Search" class="icon-small" />
{/if}
</button>
<button
@ -2455,10 +2452,7 @@ @@ -2455,10 +2452,7 @@
{#if lookupLoading[`npub-maintainers-${index}`]}
<span class="loading-text">Loading...</span>
{: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">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<img src="/icons/search.svg" alt="Search" class="icon-small" />
{/if}
</button>
{#if maintainers.length > 1}
@ -2733,7 +2727,7 @@ @@ -2733,7 +2727,7 @@
{/if}
{#if lookupResults[`doc-${index}`]}
<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>
{/if}
{/each}
@ -2789,10 +2783,7 @@ @@ -2789,10 +2783,7 @@
{#if lookupLoading['repo-forkOriginalRepo']}
<span class="loading-text">Loading...</span>
{: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">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<img src="/icons/search.svg" alt="Search" class="icon-small" />
{/if}
</button>
</div>

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

@ -1664,7 +1664,7 @@ i * @@ -1664,7 +1664,7 @@ i *
<div class="access-level-indicator">
{#if hasUnlimitedAccess(userLevel)}
<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>
{: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">

3
static/icons/check.svg

@ -0,0 +1,3 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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