diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index 984aebce1..d80f62d5a 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -4,100 +4,10 @@ import { getRandomColor } from '$lib/utils'; import { get } from 'svelte/store'; import { areas } from '$lib/store'; -import type { GiteaLabel, GiteaIssue } from '$lib/types'; - -// Cache structure with TTL -type IssuesCache = { - timestamp: number; - data: GiteaIssue[]; - totalCount: number; -}; - -let issuesCache: IssuesCache | null = null; -const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds +import type { GiteaLabel } from '$lib/types'; export type GiteaRepo = 'btcmap-data' | 'btcmap-infra'; -async function syncIssuesFromGitea(): Promise { - // Check if required environment variables are available - if (!env.GITEA_API_URL || !env.GITEA_API_KEY) { - console.warn( - 'Gitea API configuration missing (GITEA_API_URL or GITEA_API_KEY). Returning empty cache.' - ); - return { - timestamp: Date.now(), - data: [], - totalCount: 0 - }; - } - - const headers = { - Authorization: `token ${env.GITEA_API_KEY}` - }; - - const [issuesResponse, repoResponse] = await Promise.all([ - axios.get(`${env.GITEA_API_URL}/api/v1/repos/teambtcmap/btcmap-data/issues?state=open`, { - headers - }), - axios.get(`${env.GITEA_API_URL}/api/v1/repos/teambtcmap/btcmap-data`, { headers }) - ]); - - const giteaIssues = issuesResponse.data.map((issue: GiteaIssue) => ({ - id: issue.id, - number: issue.number, - title: issue.title, - created_at: issue.created_at, - html_url: issue.html_url, - labels: issue.labels, - user: { - login: issue.user.login, - avatar_url: issue.user.avatar_url, - html_url: issue.user.html_url - }, - comments: issue.comments, - assignees: issue.assignees - })); - - return { - timestamp: Date.now(), - data: giteaIssues, - totalCount: repoResponse.data.open_issues_count - }; -} - -export async function getIssues( - labelNames?: string[] -): Promise<{ issues: GiteaIssue[]; totalCount: number }> { - // Refresh cache if expired or doesn't exist - if (!issuesCache || Date.now() - issuesCache.timestamp > CACHE_DURATION) { - try { - issuesCache = await syncIssuesFromGitea(); - } catch (error) { - console.error('Failed to sync issues from Gitea:', error); - throw error; - } - } - - // If no labels specified, return all issues - if (!labelNames || labelNames.length === 0) { - return { - issues: issuesCache.data, - totalCount: issuesCache.totalCount - }; - } - - // Filter issues by labels - const filteredIssues = issuesCache.data.filter((issue) => { - const issueLabels = new Set(issue.labels.map((l) => l.name.toLowerCase())); - return labelNames.every((labelName) => issueLabels.has(labelName.toLowerCase())); - }); - - return { - issues: filteredIssues, - totalCount: filteredIssues.length - }; -} - async function getLabels(repo: GiteaRepo = 'btcmap-data'): Promise { const headers = { Authorization: `token ${env.GITEA_API_KEY}` @@ -180,11 +90,6 @@ export async function createIssueWithLabels( { headers } ); - // Only invalidate cache for btcmap-data repo - if (repo === 'btcmap-data') { - issuesCache = null; - } - return response; } catch (error) { console.error(`Failed to create issue in ${repo}:`, error); diff --git a/src/routes/api/tickets/+server.ts b/src/routes/api/tickets/+server.ts new file mode 100644 index 000000000..217238c5f --- /dev/null +++ b/src/routes/api/tickets/+server.ts @@ -0,0 +1,70 @@ +import { json } from '@sveltejs/kit'; +import axios from 'axios'; +import { env } from '$env/dynamic/private'; + +import type { RequestHandler } from './$types'; +import type { GiteaIssue } from '$lib/types'; + +async function fetchAllIssuesFromGitea(): Promise { + if (!env.GITEA_API_URL || !env.GITEA_API_KEY) { + console.warn( + 'Gitea API configuration missing (GITEA_API_URL or GITEA_API_KEY). Returning empty array.' + ); + return []; + } + + const headers = { + Authorization: `token ${env.GITEA_API_KEY}` + }; + + const allIssues: GiteaIssue[] = []; + let page = 1; + // Gitea default max is often 50, request 50 per page to be safe + const limit = 50; + + while (true) { + const response = await axios.get( + `${env.GITEA_API_URL}/api/v1/repos/teambtcmap/btcmap-data/issues?state=open&limit=${limit}&page=${page}`, + { headers } + ); + + const issues = response.data.map((issue: GiteaIssue) => ({ + id: issue.id, + number: issue.number, + title: issue.title, + created_at: issue.created_at, + html_url: issue.html_url, + labels: issue.labels, + user: { + login: issue.user.login, + avatar_url: issue.user.avatar_url, + html_url: issue.user.html_url + }, + comments: issue.comments, + assignees: issue.assignees + })); + + allIssues.push(...issues); + + // If we got fewer than requested, we've reached the end + if (response.data.length < limit) break; + page++; + } + + return allIssues; +} + +export const GET: RequestHandler = async ({ setHeaders }) => { + // Cache at edge for 24 hours, serve stale for another 24 hours while revalidating + setHeaders({ + 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=86400' + }); + + try { + const issues = await fetchAllIssuesFromGitea(); + return json({ issues, totalCount: issues.length }); + } catch (error) { + console.error('Failed to fetch issues from Gitea:', error); + return json({ issues: [], totalCount: 0, error: 'Failed to fetch issues' }, { status: 500 }); + } +}; diff --git a/src/routes/community/[area]/[section]/+page.server.ts b/src/routes/community/[area]/[section]/+page.server.ts index 2ff089453..7ba570734 100644 --- a/src/routes/community/[area]/[section]/+page.server.ts +++ b/src/routes/community/[area]/[section]/+page.server.ts @@ -1,12 +1,25 @@ import { error, redirect } from '@sveltejs/kit'; import axios from 'axios'; import axiosRetry from 'axios-retry'; + import type { PageServerLoad } from './$types'; -import { getIssues } from '$lib/gitea'; +import type { GiteaIssue, Tickets } from '$lib/types'; axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); -export const load: PageServerLoad = async ({ params }) => { +type TicketsResponse = { + issues: GiteaIssue[]; + totalCount: number; + error?: string; +}; + +function filterIssuesByLabel(issues: GiteaIssue[], labelName: string): GiteaIssue[] { + return issues.filter((issue) => + issue.labels.some((label) => label.name.toLowerCase() === labelName.toLowerCase()) + ); +} + +export const load: PageServerLoad = async ({ params, fetch }) => { const { area, section } = params; // Validate section parameter @@ -18,9 +31,15 @@ export const load: PageServerLoad = async ({ params }) => { const areaResponse = await axios.get(`https://api.btcmap.org/v2/areas/${area}`); const fetchedArea = areaResponse.data; - const { issues: tickets } = await getIssues([fetchedArea.tags.url_alias]).catch(() => ({ - issues: 'error' - })); + // Fetch from cached /api/tickets endpoint and filter by area label + let tickets: Tickets; + try { + const ticketsResponse = await fetch('/api/tickets'); + const ticketsData: TicketsResponse = await ticketsResponse.json(); + tickets = filterIssuesByLabel(ticketsData.issues, fetchedArea.tags.url_alias); + } catch { + tickets = 'error'; + } const issuesResponse = await fetch('https://api.btcmap.org/rpc', { method: 'POST', diff --git a/src/routes/country/[area]/[section]/+page.server.ts b/src/routes/country/[area]/[section]/+page.server.ts index 67f2b8f06..9a93cc70a 100644 --- a/src/routes/country/[area]/[section]/+page.server.ts +++ b/src/routes/country/[area]/[section]/+page.server.ts @@ -1,12 +1,25 @@ import { error, redirect } from '@sveltejs/kit'; import axios from 'axios'; import axiosRetry from 'axios-retry'; + import type { PageServerLoad } from './$types'; -import { getIssues } from '$lib/gitea'; +import type { GiteaIssue, Tickets } from '$lib/types'; axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); -export const load: PageServerLoad = async ({ params }) => { +type TicketsResponse = { + issues: GiteaIssue[]; + totalCount: number; + error?: string; +}; + +function filterIssuesByLabel(issues: GiteaIssue[], labelName: string): GiteaIssue[] { + return issues.filter((issue) => + issue.labels.some((label) => label.name.toLowerCase() === labelName.toLowerCase()) + ); +} + +export const load: PageServerLoad = async ({ params, fetch }) => { const { area, section } = params; // Validate section parameter - default to merchants if not provided @@ -20,9 +33,15 @@ export const load: PageServerLoad = async ({ params }) => { const areaResponse = await axios.get(`https://api.btcmap.org/v2/areas/${area}`); const fetchedArea = areaResponse.data; - const { issues: tickets } = await getIssues([fetchedArea.tags.url_alias]).catch(() => ({ - issues: 'error' - })); + // Fetch from cached /api/tickets endpoint and filter by area label + let tickets: Tickets; + try { + const ticketsResponse = await fetch('/api/tickets'); + const ticketsData: TicketsResponse = await ticketsResponse.json(); + tickets = filterIssuesByLabel(ticketsData.issues, fetchedArea.tags.url_alias); + } catch { + tickets = 'error'; + } const issuesResponse = await fetch('https://api.btcmap.org/rpc', { method: 'POST', diff --git a/src/routes/tickets/+page.server.ts b/src/routes/tickets/+page.server.ts index f8eb7aac5..d5ea9d540 100644 --- a/src/routes/tickets/+page.server.ts +++ b/src/routes/tickets/+page.server.ts @@ -1,12 +1,20 @@ -import { getIssues } from '$lib/gitea'; +import type { GiteaIssue } from '$lib/types'; import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async () => { +type TicketsResponse = { + issues: GiteaIssue[]; + totalCount: number; + error?: string; +}; + +export const load: PageServerLoad = async ({ fetch }) => { try { - const { issues, totalCount } = await getIssues(); + const response = await fetch('/api/tickets'); + const data: TicketsResponse = await response.json(); + return { - tickets: issues, - totalTickets: totalCount + tickets: data.issues, + totalTickets: data.totalCount }; } catch (error) { console.error('Failed to fetch issues:', error);