Browse Source

remove polling

Nostr-Signature: 40f01e84f96661bb7fea13aa63c7da428118061b0a1470a11890d4f9cd6d685b 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc dbb6947defac6c7f92a3cf6f72352a94ffe2c4b33e65f8410518a40406c93f1f5a3e13e81f2f04f676d826e6cf03ec802328f5228300f80a8114fa3fd26eaeff
main
Silberengel 2 weeks ago
parent
commit
7bb1c8d66b
  1. 1
      docs/api-and-cli.md
  2. 10
      docs/editing-repos.md
  3. 1
      nostr/commit-signatures.jsonl
  4. 50
      src/hooks.server.ts
  5. 5
      src/lib/services/git/repo-manager.ts
  6. 278
      src/lib/services/nostr/repo-polling.ts
  7. 16
      src/lib/services/service-registry.ts
  8. 1
      src/lib/utils/nostr-utils.ts
  9. 30
      src/lib/utils/repo-poll-trigger.ts
  10. 43
      src/routes/api/openapi.json/openapi.json
  11. 2
      src/routes/api/repos/local/+server.ts
  12. 33
      src/routes/api/repos/poll/+server.ts
  13. 7
      src/routes/api/user/level/+server.ts
  14. 29
      src/routes/repos/+page.svelte

1
docs/api-and-cli.md

@ -116,7 +116,6 @@ View interactive documentation at `/api/openapi.json` or use any OpenAPI viewer. @@ -116,7 +116,6 @@ View interactive documentation at `/api/openapi.json` or use any OpenAPI viewer.
- `GET /api/config` - Get server configuration
- `GET /api/tor/onion` - Get Tor .onion address
- `POST /api/repos/poll` - Trigger repository polling (provisions new repos from Nostr)
- `GET /api/transfers/pending` - Get pending ownership transfers
#### Git HTTP Backend

10
docs/editing-repos.md

@ -2,18 +2,18 @@ @@ -2,18 +2,18 @@
This page covers all aspects of editing repositories: branch management, file management, auto-provisioning, file-editing permissions, and event-creation permissions.
## Auto-Provisioning
## Adding Repositories
When you create a repository announcement, GitRepublic automatically:
Repositories must be explicitly added to the server using the clone endpoint. When you clone a repository:
1. **Polls Nostr relays** for new announcements
1. **Fetches the repository announcement** from Nostr relays
2. **Creates a bare git repository** at `/repos/{npub}/{repo-name}.git`
3. **Fetches self-transfer event** for ownership verification
3. **Fetches self-transfer event** for ownership verification (if available)
4. **Creates initial commit** with README.md (if provided)
5. **Saves announcement and transfer events** to `nostr/repo-events.jsonl`
6. **Syncs from other remotes** if clone URLs are configured
The repository is ready to use immediately after announcement.
The repository is ready to use immediately after cloning.
## Branch Management

1
nostr/commit-signatures.jsonl

@ -120,3 +120,4 @@ @@ -120,3 +120,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772269280,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","api refactor part 2"]],"content":"Signed commit: api refactor part 2","id":"ece894a60057bba46ebd4ac0dca2aca55ffce05e44671fe07b29516809fc86f6","sig":"176706a271659834e441ea5eab4bb1480667dad4468fe8315803284f4a183debf595523dd33d0d3cabe0c35013f4a72b9169b5f10afefaf8a82a721d8b0f3b08"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772270859,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes and fallback relay"]],"content":"Signed commit: bug-fixes and fallback relay","id":"1d85d0c5e1451c90bca5d59e08043f29adeaad4db4ac5495c8e9a4247775780f","sig":"a1960b76c78db9f64dad20378d26f500ffc09f1f6d137314db548470202712222a1d391f682146ba281fd23355c574fcbb260310db61b3458bba3dec0c724a18"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772271656,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"f4a5e0d3e2aa7d0d99803f26008ab68e40551e36362bb6d04acf639c5b78d959","sig":"59da9e59a6fb5648f4c889e0045b571e0d2d66a555100d60dec373455309a640bea89e4bb3a42a0e502aa4d2091e4b698203721e79b346ff30e6b2bcdc5f48b3"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772274086,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"32794e047f06902ad610f918834efb113f41eace26a53a3f0fad083b9d8323dc","sig":"3859f0de3de0f8a742b6fbe7709c5a5625f4d5612a936fd81f38a7e1231ee810b50a69c1ed5d23c8a6670b4cbc9ea3d4bd39d6fa9e6207802f45995689b924a9"}

50
src/hooks.server.ts

@ -1,23 +1,14 @@ @@ -1,23 +1,14 @@
/**
* Server-side hooks for gitrepublic-web
* Initializes repo polling service and security middleware
* Initializes security middleware
*/
import type { Handle } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import { RepoPollingService } from './lib/services/nostr/repo-polling.js';
import { GIT_DOMAIN, DEFAULT_NOSTR_RELAYS } from './lib/config.js';
import { setRepoPollingService } from './lib/services/service-registry.js';
import { rateLimiter } from './lib/services/security/rate-limiter.js';
import { auditLogger } from './lib/services/security/audit-logger.js';
import logger from './lib/services/logger.js';
// Initialize polling service
const repoRoot = process.env.GIT_REPO_ROOT || '/repos';
const domain = GIT_DOMAIN;
let pollingService: RepoPollingService | null = null;
if (typeof process !== 'undefined') {
// Handle unhandled promise rejections to prevent crashes from relay errors
process.on('unhandledRejection', (reason, promise) => {
@ -29,29 +20,9 @@ if (typeof process !== 'undefined') { @@ -29,29 +20,9 @@ if (typeof process !== 'undefined') {
}
});
pollingService = new RepoPollingService(DEFAULT_NOSTR_RELAYS, repoRoot, domain);
// Register with service registry so it can be accessed from API endpoints
setRepoPollingService(pollingService);
// Start polling - the initial poll will complete asynchronously
// The local repos endpoint will skip cache for the first 10 seconds after startup
pollingService.start().then(() => {
logger.info({ service: 'repo-polling', relays: DEFAULT_NOSTR_RELAYS.length }, 'Repo polling service ready (initial poll completed)');
}).catch((err) => {
logger.error({ error: err, service: 'repo-polling' }, 'Initial repo poll failed, but continuing');
});
logger.info({ service: 'repo-polling', relays: DEFAULT_NOSTR_RELAYS.length }, 'Started repo polling service (initial poll in progress)');
// Cleanup on server shutdown
const cleanup = (signal: string) => {
logger.info({ signal }, 'Received shutdown signal, cleaning up...');
if (pollingService) {
logger.info('Stopping repo polling service...');
pollingService.stop();
pollingService = null;
}
// Give a moment for cleanup, then exit
setTimeout(() => {
process.exit(0);
@ -68,25 +39,6 @@ if (typeof process !== 'undefined') { @@ -68,25 +39,6 @@ if (typeof process !== 'undefined') {
process.exit(0);
}, 2000);
});
// Also cleanup on process exit (last resort)
process.on('exit', () => {
if (pollingService) {
pollingService.stop();
}
});
// Periodic zombie process cleanup check
// This helps catch any processes that weren't properly cleaned up
if (typeof setInterval !== 'undefined') {
setInterval(() => {
// Check for zombie processes by attempting to reap them
// Node.js handles this automatically via 'close' events, but this is a safety net
// We can't directly check for zombies, but we can ensure our cleanup is working
// The real cleanup happens in process handlers, this is just monitoring
logger.debug('Zombie cleanup check (process handlers should prevent zombies)');
}, 60000); // Check every minute
}
}
export const handle: Handle = async ({ event, resolve }) => {

5
src/lib/services/git/repo-manager.ts

@ -182,11 +182,10 @@ export class RepoManager { @@ -182,11 +182,10 @@ export class RepoManager {
} else {
// For existing repos, check if announcement exists in repo
// If not, try to fetch from relays and save it
// Note: We have the announcement from polling (event parameter), so we can use that
// Non-blocking: fire and forget - we have the announcement from relays, so this is just for offline papertrail
// Note: We have the announcement from the clone request (event parameter), so we can use that
const hasAnnouncement = await this.announcementManager.hasAnnouncementInRepoFile(repoPath.fullPath);
if (!hasAnnouncement) {
// We have the event from polling, so use it directly (no need to fetch from relays again)
// We have the event from the clone request, so use it directly (no need to fetch from relays again)
// Save announcement to repo asynchronously
this.announcementManager.ensureAnnouncementInRepo(repoPath.fullPath, event, selfTransferEvent)
.catch((err) => {

278
src/lib/services/nostr/repo-polling.ts

@ -1,278 +0,0 @@ @@ -1,278 +0,0 @@
/**
* Service for polling NIP-34 repo announcements and auto-provisioning repos
*/
import { NostrClient } from './nostr-client.js';
import { KIND } from '../../types/nostr.js';
import type { NostrEvent } from '../../types/nostr.js';
import { RepoManager } from '../git/repo-manager.js';
import { OwnershipTransferService } from './ownership-transfer-service.js';
import { getCachedUserLevel } from '../security/user-level-cache.js';
import logger from '../logger.js';
import { extractCloneUrls } from '../../utils/nostr-utils.js';
export class RepoPollingService {
private nostrClient: NostrClient;
private repoManager: RepoManager;
private pollingInterval: number;
private intervalId: NodeJS.Timeout | null = null;
private domain: string;
private relays: string[];
private initialPollPromise: Promise<void> | null = null;
private isInitialPollComplete: boolean = false;
constructor(
relays: string[],
repoRoot: string,
domain: string,
pollingInterval: number = 60000 // 1 minute
) {
this.relays = relays;
this.nostrClient = new NostrClient(relays);
this.repoManager = new RepoManager(repoRoot, domain);
this.pollingInterval = pollingInterval;
this.domain = domain;
}
/**
* Start polling for repo announcements
* Returns a promise that resolves when the initial poll completes
*/
start(): Promise<void> {
if (this.intervalId) {
this.stop();
}
// Poll immediately and wait for it to complete
this.initialPollPromise = this.poll();
// Then poll at intervals
this.intervalId = setInterval(() => {
this.poll();
}, this.pollingInterval);
return this.initialPollPromise;
}
/**
* Wait for initial poll to complete (useful for server startup)
*/
async waitForInitialPoll(): Promise<void> {
if (this.initialPollPromise) {
await this.initialPollPromise;
}
}
/**
* Check if initial poll has completed
*/
isReady(): boolean {
return this.isInitialPollComplete;
}
/**
* Stop polling and cleanup resources
*/
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
// Close Nostr client connections
if (this.nostrClient) {
this.nostrClient.close();
}
}
/**
* Trigger a manual poll (useful after user verification)
*/
async triggerPoll(): Promise<void> {
logger.info('Manual poll triggered');
return this.poll();
}
/**
* Poll for new repo announcements and provision repos
*/
private async poll(): Promise<void> {
try {
logger.debug('Starting repo poll...');
const events = await this.nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
limit: 100
}
]);
// Filter for repos that list our domain
const relevantEvents = events.filter(event => {
// Skip local-only forks (synthetic announcements not published to Nostr)
const isLocalOnly = event.tags.some(t => t[0] === 'local-only' && t[1] === 'true');
if (isLocalOnly) {
return false;
}
const cloneUrls = this.extractCloneUrls(event);
const listsDomain = cloneUrls.some(url => url.includes(this.domain));
if (listsDomain) {
logger.debug({
eventId: event.id,
pubkey: event.pubkey.slice(0, 16) + '...',
cloneUrls: cloneUrls.slice(0, 3) // Log first 3 URLs
}, 'Found repo announcement that lists this domain');
}
return listsDomain;
});
logger.info({
totalEvents: events.length,
relevantEvents: relevantEvents.length,
domain: this.domain
}, 'Filtered repo announcements');
// Provision each repo
for (const event of relevantEvents) {
try {
// Extract repo ID from d-tag
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
if (!dTag) {
logger.warn({ eventId: event.id }, 'Repo announcement missing d-tag');
continue;
}
// Check if this is an existing repo or new repo
const cloneUrls = this.extractCloneUrls(event);
const domainUrl = cloneUrls.find(url => url.includes(this.domain));
if (!domainUrl) continue;
const repoPath = this.repoManager.parseRepoUrl(domainUrl);
if (!repoPath) continue;
const repoExists = this.repoManager.repoExists(repoPath.fullPath);
const isExistingRepo = repoExists;
// Fetch self-transfer event for this repo
const ownershipService = new OwnershipTransferService(this.relays);
const repoTag = `${KIND.REPO_ANNOUNCEMENT}:${event.pubkey}:${dTag}`;
const selfTransferEvents = await this.nostrClient.fetchEvents([
{
kinds: [KIND.OWNERSHIP_TRANSFER],
'#a': [repoTag],
authors: [event.pubkey],
limit: 10
}
]);
// Find self-transfer event (from owner to themselves)
let selfTransferEvent: NostrEvent | undefined;
for (const transferEvent of selfTransferEvents) {
const pTag = transferEvent.tags.find(t => t[0] === 'p');
if (pTag && pTag[1] === event.pubkey) {
// Decode npub if needed
let toPubkey = pTag[1];
try {
const { nip19 } = await import('nostr-tools');
const decoded = nip19.decode(toPubkey);
if (decoded.type === 'npub') {
toPubkey = decoded.data as string;
}
} catch {
// Assume it's already hex
}
if (transferEvent.pubkey === event.pubkey && toPubkey === event.pubkey) {
selfTransferEvent = transferEvent;
break;
}
}
}
// For existing repos without self-transfer, create one retroactively
if (isExistingRepo && !selfTransferEvent) {
// Security: Truncate pubkey in logs
const truncatedPubkey = event.pubkey.length > 16 ? `${event.pubkey.slice(0, 8)}...${event.pubkey.slice(-4)}` : event.pubkey;
logger.info({ repoId: dTag, pubkey: truncatedPubkey }, 'Existing repo has no self-transfer event. Creating template for owner to sign and publish.');
try {
// Create a self-transfer event template for the existing repo
// The owner will need to sign and publish this to relays
const initialOwnershipEvent = ownershipService.createInitialOwnershipEvent(event.pubkey, dTag);
// Create an unsigned event template that can be included in the repo
// This serves as a reference and the owner can use it to create the actual event
const selfTransferTemplate = {
...initialOwnershipEvent,
id: '', // Will be computed when signed
sig: '', // Needs owner signature
_note: 'This is a template. The owner must sign and publish this event to relays for it to be valid.'
} as NostrEvent & { _note?: string };
// Use the template (even though it's unsigned, it will be included in the repo)
selfTransferEvent = selfTransferTemplate;
logger.warn({ repoId: dTag, pubkey: event.pubkey }, 'Self-transfer event template created. Owner should sign and publish it to relays.');
} catch (err) {
logger.error({ error: err, repoId: dTag }, 'Failed to create self-transfer event template');
}
}
// Check if user has unlimited access before provisioning new repos
// This prevents spam and abuse
if (!isExistingRepo) {
const userLevel = getCachedUserLevel(event.pubkey);
const { hasUnlimitedAccess } = await import('../../utils/user-access.js');
const hasAccess = hasUnlimitedAccess(userLevel?.level);
logger.debug({
eventId: event.id,
pubkey: event.pubkey.slice(0, 16) + '...',
cachedLevel: userLevel?.level || 'none',
hasAccess,
isExistingRepo
}, 'Checking user access for repo provisioning');
if (!hasAccess) {
logger.warn({
eventId: event.id,
pubkey: event.pubkey.slice(0, 16) + '...',
level: userLevel?.level || 'none',
cacheExists: !!userLevel
}, 'Skipping repo provisioning: user does not have unlimited access');
continue;
}
}
// Provision the repo with self-transfer event if available
await this.repoManager.provisionRepo(event, selfTransferEvent, isExistingRepo);
logger.info({ eventId: event.id, isExistingRepo }, 'Provisioned repo from announcement');
} catch (error) {
logger.error({ error, eventId: event.id }, 'Failed to provision repo from announcement');
}
}
// Mark initial poll as complete
if (!this.isInitialPollComplete) {
this.isInitialPollComplete = true;
logger.info('Initial repo poll completed');
}
} catch (error) {
logger.error({ error }, 'Error polling for repo announcements');
// Still mark as complete even on error (to prevent blocking)
if (!this.isInitialPollComplete) {
this.isInitialPollComplete = true;
logger.warn('Initial repo poll completed with errors');
}
}
}
/**
* Extract clone URLs from a NIP-34 repo announcement
* Uses shared utility (without normalization)
*/
private extractCloneUrls(event: NostrEvent): string[] {
return extractCloneUrls(event, false);
}
}

16
src/lib/services/service-registry.ts

@ -15,7 +15,6 @@ import { ForkCountService } from './nostr/fork-count-service.js'; @@ -15,7 +15,6 @@ import { ForkCountService } from './nostr/fork-count-service.js';
import { PRsService } from './nostr/prs-service.js';
import { HighlightsService } from './nostr/highlights-service.js';
import { ReleasesService } from './nostr/releases-service.js';
import { RepoPollingService } from './nostr/repo-polling.js';
import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, GIT_DOMAIN } from '../config.js';
// Get repo root from environment or use default
@ -36,7 +35,6 @@ let _forkCountService: ForkCountService | null = null; @@ -36,7 +35,6 @@ let _forkCountService: ForkCountService | null = null;
let _prsService: PRsService | null = null;
let _highlightsService: HighlightsService | null = null;
let _releasesService: ReleasesService | null = null;
let _repoPollingService: RepoPollingService | null = null;
/**
* Get singleton FileManager instance
@ -158,20 +156,6 @@ export function getReleasesService(): ReleasesService { @@ -158,20 +156,6 @@ export function getReleasesService(): ReleasesService {
return _releasesService;
}
/**
* Get singleton RepoPollingService instance
* Note: This should be initialized in hooks.server.ts on startup
*/
export function getRepoPollingService(): RepoPollingService | null {
return _repoPollingService;
}
/**
* Set the RepoPollingService instance (called from hooks.server.ts)
*/
export function setRepoPollingService(service: RepoPollingService): void {
_repoPollingService = service;
}
// Convenience exports for direct access (common pattern)
export const fileManager = getFileManager();

1
src/lib/utils/nostr-utils.ts

@ -12,7 +12,6 @@ import { KIND } from '../types/nostr.js'; @@ -12,7 +12,6 @@ import { KIND } from '../types/nostr.js';
* This is a shared utility to avoid code duplication across:
* - RepoManager (with URL normalization)
* - Git API endpoint (for performance, without normalization)
* - RepoPollingService
*
* @param event - The Nostr repository announcement event
* @param normalize - Whether to normalize URLs (add .git suffix if needed). Default: false

30
src/lib/utils/repo-poll-trigger.ts

@ -1,30 +0,0 @@ @@ -1,30 +0,0 @@
/**
* Shared utility for triggering repo polls
* This provides a consistent interface for triggering polls from anywhere in the codebase
*/
import { getRepoPollingService } from '../services/service-registry.js';
import logger from '../services/logger.js';
/**
* Trigger a repo poll
* This is the single source of truth for triggering polls
* @param context Optional context string for logging (e.g., 'user-verification', 'manual-refresh')
* @returns Promise that resolves when poll is triggered (not when it completes)
*/
export async function triggerRepoPoll(context?: string): Promise<void> {
const pollingService = getRepoPollingService();
if (!pollingService) {
logger.warn({ context }, 'Poll request received but polling service not initialized');
throw new Error('Polling service not available');
}
// Trigger poll asynchronously (non-blocking)
// The poll will complete in the background
pollingService.triggerPoll().catch((err) => {
logger.error({ error: err, context }, 'Failed to trigger poll');
});
logger.info({ context }, 'Repo poll triggered');
}

43
src/routes/api/openapi.json/openapi.json

@ -286,49 +286,6 @@ @@ -286,49 +286,6 @@
}
}
},
"/api/repos/poll": {
"post": {
"summary": "Trigger repository polling",
"description": "Manually trigger repository polling to provision new repos from Nostr announcements. This endpoint fetches NIP-34 repo announcements from relays and provisions repositories that list this server's domain in their clone URLs. The poll runs asynchronously and does not block the request.",
"tags": ["Infrastructure"],
"responses": {
"200": {
"description": "Poll triggered successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {"type": "boolean"},
"message": {"type": "string"}
}
}
}
}
},
"503": {
"description": "Polling service not available",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Error triggering poll",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/repos/{npub}/{repo}/files": {
"get": {
"summary": "Get file content, list files, or get raw file",

2
src/routes/api/repos/local/+server.ts

@ -35,7 +35,7 @@ let cache: CacheEntry | null = null; @@ -35,7 +35,7 @@ let cache: CacheEntry | null = null;
// Track server startup time to invalidate cache on first request after startup
let serverStartTime = Date.now();
const STARTUP_GRACE_PERIOD = 10000; // 10 seconds - allow time for initial poll
const STARTUP_GRACE_PERIOD = 1000; // 1 second - minimal grace period for cache
/**
* Invalidate cache (internal use only - not exported to avoid SvelteKit build errors)

33
src/routes/api/repos/poll/+server.ts

@ -1,33 +0,0 @@ @@ -1,33 +0,0 @@
/**
* API endpoint for manually triggering a repo poll
* This allows users to refresh the repo list and trigger provisioning of new repos
*
* This is the public API interface for triggering polls.
* All poll triggers should go through this endpoint or the shared triggerRepoPoll utility.
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { triggerRepoPoll } from '$lib/utils/repo-poll-trigger.js';
import { extractRequestContext } from '$lib/utils/api-context.js';
export const POST: RequestHandler = async (event) => {
const requestContext = extractRequestContext(event);
const clientIp = requestContext.clientIp || 'unknown';
try {
await triggerRepoPoll('api-endpoint');
return json({
success: true,
message: 'Poll triggered successfully'
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
return json({
success: false,
error: errorMessage
}, { status: err instanceof Error && errorMessage.includes('not available') ? 503 : 500 });
}
};

7
src/routes/api/user/level/+server.ts

@ -17,7 +17,6 @@ import { extractRequestContext } from '$lib/utils/api-context.js'; @@ -17,7 +17,6 @@ import { extractRequestContext } from '$lib/utils/api-context.js';
import { sanitizeError } from '$lib/utils/security.js';
import { verifyEvent } from 'nostr-tools';
import logger from '$lib/services/logger.js';
import { triggerRepoPoll } from '$lib/utils/repo-poll-trigger.js';
export const POST: RequestHandler = async (event) => {
const requestContext = extractRequestContext(event);
@ -144,12 +143,6 @@ export const POST: RequestHandler = async (event) => { @@ -144,12 +143,6 @@ export const POST: RequestHandler = async (event) => {
// Cache the successful verification
cacheUserLevel(userPubkeyHex, 'unlimited');
// Trigger a repo poll to provision repos now that user is verified
// This is non-blocking - we don't wait for it to complete
triggerRepoPoll('user-verification').catch((err) => {
logger.warn({ error: err, userPubkeyHex }, 'Failed to trigger poll after user verification (non-blocking)');
});
auditLogger.logAuth(
userPubkeyHex,
clientIp,

29
src/routes/repos/+page.svelte

@ -308,7 +308,7 @@ @@ -308,7 +308,7 @@
}
}
async function loadRepos(triggerPoll = false) {
async function loadRepos() {
loading = true;
error = null;
@ -357,31 +357,6 @@ @@ -357,31 +357,6 @@
loadForkCounts(registeredRepos.map(r => r.event)).catch(err => {
console.warn('[RepoList] Failed to load some fork counts:', err);
});
// If triggerPoll is true, trigger a poll and then refresh the list
if (triggerPoll) {
try {
// Trigger poll (non-blocking)
const pollResponse = await fetch('/api/repos/poll', {
method: 'POST',
headers: userPubkeyHex ? {
'X-User-Pubkey': userPubkeyHex
} : {}
});
if (pollResponse.ok) {
// Wait a bit for the poll to process (lazy - don't wait for full completion)
// Give it 2-3 seconds to provision repos
await new Promise(resolve => setTimeout(resolve, 2500));
// Refresh the list after poll
await loadRepos(false);
}
} catch (pollErr) {
// Don't fail the whole operation if poll fails
console.warn('[RepoList] Failed to trigger poll:', pollErr);
}
}
} catch (e) {
error = String(e);
console.error('[RepoList] Failed to load repos:', e);
@ -802,7 +777,7 @@ @@ -802,7 +777,7 @@
<div class="repos-header">
<h2>Repositories on {$page.data.gitDomain || 'localhost:6543'}</h2>
<button onclick={() => loadRepos(true)} disabled={loading}>
<button onclick={() => loadRepos()} disabled={loading}>
{loading ? 'Loading...' : 'Refresh'}
</button>
</div>

Loading…
Cancel
Save