Browse Source

support issues

master
Silberengel 4 weeks ago
parent
commit
8ef73f7a62
  1. 168
      src/routes/repos/[naddr]/+page.svelte
  2. 12
      src/routes/write/+page.svelte
  3. 1
      static/changelog.yaml
  4. 4
      static/healthz.json

168
src/routes/repos/[naddr]/+page.svelte

@ -418,7 +418,7 @@ @@ -418,7 +418,7 @@
}
}
async function loadIssueStatuses() {
async function loadIssueStatuses(forceNetwork = false) {
if (issues.length === 0) return;
try {
@ -438,6 +438,7 @@ @@ -438,6 +438,7 @@
// Status events are different kinds: 1630 (Open), 1631 (Applied/Merged/Resolved), 1632 (Closed), 1633 (Draft)
// They have "e" tags pointing to issues with marker "root"
// Use 'relay-first' when forcing network fetch (e.g., after status change) to bypass cached empty results
const statuses = await nostrClient.fetchEvents(
[{
'#e': issueIds,
@ -445,7 +446,7 @@ @@ -445,7 +446,7 @@
limit: 200
}],
relays,
{ useCache: 'cache-first', cacheResults: true } // Prioritize cache
{ useCache: forceNetwork ? 'relay-first' : 'cache-first', cacheResults: true }
);
// Process statuses for this batch
@ -486,8 +487,18 @@ @@ -486,8 +487,18 @@
}
}
// Merge with existing state to preserve any local updates that haven't propagated yet
// Keep the newer status for each issue
const mergedStatuses = new Map(issueStatuses);
for (const [issueId, newStatus] of statusMap.entries()) {
const existingStatus = mergedStatuses.get(issueId);
if (!existingStatus || newStatus.created_at > existingStatus.created_at) {
mergedStatuses.set(issueId, newStatus);
}
}
// Update state after each batch so newest issues show statuses first
issueStatuses = new Map(statusMap);
issueStatuses = mergedStatuses;
}
} catch (error) {
// Failed to load issue statuses
@ -552,6 +563,12 @@ @@ -552,6 +563,12 @@
issueStatuses.set(issueId, signedEvent);
issueStatuses = new Map(issueStatuses); // Trigger reactivity
// Refresh the view immediately using cache-first to show the newly cached event
// This ensures the cached event is displayed right away
loadIssueStatuses(false).catch(() => {
// Non-critical - status is already updated locally
});
// Publish to relays in background (don't wait for it)
const relays = relayManager.getProfileReadRelays();
signAndPublish(event, relays).then((result) => {
@ -559,10 +576,13 @@ @@ -559,10 +576,13 @@
console.warn('Failed to publish status change to some relays:', result.failed);
// Don't show alert - event is cached and will sync eventually
}
// Reload statuses to get any other updates from relays
loadIssueStatuses().catch(() => {
// Non-critical - status is already updated locally
});
// Optionally reload from network to get any other updates from relays
// Use a small delay to allow the event to propagate
setTimeout(() => {
loadIssueStatuses(true).catch(() => {
// Non-critical - status is already updated locally
});
}, 1000);
}).catch((error) => {
console.error('Error publishing status change:', error);
// Don't show alert - event is cached and will sync eventually
@ -854,6 +874,15 @@ @@ -854,6 +874,15 @@
}
}
function getRepoATag(): string | null {
if (!repoEvent) return null;
const dTag = repoEvent.tags.find(t => Array.isArray(t) && t[0] === 'd')?.[1] || '';
if (dTag) {
return `${repoEvent.kind}:${repoEvent.pubkey}:${dTag}`;
}
return null;
}
function getRepoName(): string {
try {
if (!repoEvent) return 'Repository';
@ -1470,31 +1499,40 @@ @@ -1470,31 +1499,40 @@
</div>
{:else if issues.length > 0}
<div class="issues-filter">
<label for="status-filter" class="filter-label">Filter by status:</label>
<select
id="status-filter"
value={statusFilter || 'all'}
onchange={(e) => {
const value = (e.target as HTMLSelectElement).value;
statusFilter = value === 'all' ? null : value;
}}
class="status-filter-select"
>
<option value="all">All</option>
{#each availableStatuses as statusOption}
<option value={statusOption}>{statusOption}</option>
{/each}
</select>
<span class="filter-count">
{#if statusFilter}
Showing {filteredIssues.length} of {issues.length} issues
{:else}
{issues.length} {issues.length === 1 ? 'issue' : 'issues'}
{/if}
{#if loadingIssueData}
<span class="loading-indicator"> (loading details...)</span>
{/if}
</span>
<div class="issues-filter-left">
<label for="status-filter" class="filter-label">Filter by status:</label>
<select
id="status-filter"
value={statusFilter || 'all'}
onchange={(e) => {
const value = (e.target as HTMLSelectElement).value;
statusFilter = value === 'all' ? null : value;
}}
class="status-filter-select"
>
<option value="all">All</option>
{#each availableStatuses as statusOption}
<option value={statusOption}>{statusOption}</option>
{/each}
</select>
<span class="filter-count">
{#if statusFilter}
Showing {filteredIssues.length} of {issues.length} issues
{:else}
{issues.length} {issues.length === 1 ? 'issue' : 'issues'}
{/if}
{#if loadingIssueData}
<span class="loading-indicator"> (loading details...)</span>
{/if}
</span>
</div>
{#if sessionManager.getSession()}
{@const repoATag = getRepoATag()}
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue">
<Icon name="edit" size={16} />
<span>New Issue</span>
</a>
{/if}
</div>
<div class="issues-list">
{#if filteredIssues.length > 0}
@ -1577,12 +1615,26 @@ @@ -1577,12 +1615,26 @@
{:else}
<div class="empty-state">
<p class="text-fog-text dark:text-fog-dark-text">No issues found with status "{statusFilter}".</p>
{#if sessionManager.getSession()}
{@const repoATag = getRepoATag()}
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue">
<Icon name="edit" size={16} />
<span>New Issue</span>
</a>
{/if}
</div>
{/if}
</div>
{:else}
<div class="empty-state">
<p class="text-fog-text dark:text-fog-dark-text">No issues found.</p>
{#if sessionManager.getSession()}
{@const repoATag = getRepoATag()}
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue">
<Icon name="edit" size={16} />
<span>New Issue</span>
</a>
{/if}
</div>
{/if}
</div>
@ -1634,6 +1686,10 @@ @@ -1634,6 +1686,10 @@
.empty-state {
padding: 2rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.repo-banner-container {
@ -1986,12 +2042,14 @@ @@ -1986,12 +2042,14 @@
.issues-filter {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--fog-highlight, #f3f4f6);
border-radius: 0.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
flex-wrap: wrap;
}
:global(.dark) .issues-filter {
@ -1999,6 +2057,14 @@ @@ -1999,6 +2057,14 @@
border-color: var(--fog-dark-border, #374151);
}
.issues-filter-left {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
flex: 1;
}
.filter-label {
font-weight: 500;
color: var(--fog-text, #1f2937);
@ -2046,7 +2112,6 @@ @@ -2046,7 +2112,6 @@
}
.filter-count {
margin-left: auto;
font-size: 0.875rem;
color: var(--fog-text-light, #52667a);
}
@ -2055,6 +2120,43 @@ @@ -2055,6 +2120,43 @@
color: var(--fog-dark-text-light, #a8b8d0);
}
.see-more-events-btn-header {
padding: 0.5rem 1rem;
border: 1px solid var(--fog-border, #cbd5e1);
border-radius: 4px;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1e293b);
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.see-more-events-btn-header:hover:not(:disabled) {
background: var(--fog-highlight, #f1f5f9);
border-color: var(--fog-accent, #94a3b8);
}
.see-more-events-btn-header:disabled {
opacity: 0.5;
cursor: not-allowed;
}
:global(.dark) .see-more-events-btn-header {
background: var(--fog-dark-post, #334155);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f1f5f9);
}
:global(.dark) .see-more-events-btn-header:hover:not(:disabled) {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-accent, #64748b);
}
.issues-list {
display: flex;
flex-direction: column;

12
src/routes/write/+page.svelte

@ -9,8 +9,9 @@ @@ -9,8 +9,9 @@
import type { NostrEvent } from '../../lib/types/nostr.js';
// Read kind from URL synchronously so it's available on first render
// Read kind and a-tag from URL synchronously so it's available on first render
const kindParam = $derived($page.url.searchParams.get('kind'));
const aTagParam = $derived($page.url.searchParams.get('a'));
let initialEvent = $state<NostrEvent | null>(null);
let isCloneMode = $state(false);
@ -65,18 +66,23 @@ @@ -65,18 +66,23 @@
}
}
// Set initial kind from URL if available (only if no event from sessionStorage)
// Set initial kind and tags from URL if available (only if no event from sessionStorage)
$effect(() => {
if (kindParam && !initialEvent) {
const kind = parseInt(kindParam, 10);
if (!isNaN(kind)) {
const tags: string[][] = [];
// Add a-tag if provided (for repo references in issues)
if (aTagParam) {
tags.push(['a', aTagParam]);
}
initialEvent = {
id: '',
pubkey: '',
created_at: Math.floor(Date.now() / 1000),
kind: kind,
content: '',
tags: [],
tags: tags,
sig: ''
};
}

1
static/changelog.yaml

@ -2,6 +2,7 @@ versions: @@ -2,6 +2,7 @@ versions:
'0.3.3':
- 'Added GRASP repository management'
- 'Support manual creation and editing of User Grasp List (kind 10317) and repo announcements (kind 30617)'
- 'Support creation of issues in repositories'
'0.3.2':
- 'Expanded /repos to handle GitLab, Gitea, and OneDev repositories'
- 'Added back and refresh buttons to all pages'

4
static/healthz.json

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
"status": "ok",
"service": "aitherboard",
"version": "0.3.3",
"buildTime": "2026-02-15T06:43:22.492Z",
"buildTime": "2026-02-15T12:37:12.663Z",
"gitCommit": "unknown",
"timestamp": 1771137802493
"timestamp": 1771159032663
}
Loading…
Cancel
Save