feat: add progress endpoint

This commit is contained in:
mrjvs
2026-08-08 22:10:13 +02:00
parent a41fc8c8a0
commit f3ad06c650
9 changed files with 1002 additions and 1 deletions

View File

@@ -0,0 +1,172 @@
import { Octokit } from "octokit";
export type GithubProjectTaskStatus = 'completed' | 'inprogress' | 'notstarted';
export type GithubProject = {
title: string;
url: string;
tasks: Array<{
title: string;
status: GithubProjectTaskStatus,
}>
}
export type GithubProjectResponse = {
projects: GithubProject[];
}
const orgName = "PretendoNetwork";
const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour
let cache: { response: GithubProjectResponse, createdAt: Date } | null = null;
const getProjectsV2GQL = `
query getProjectsV2($orgName: String!, $cursor: String) {
organization(login: $orgName) {
projectsV2(first: 10, after: $cursor) {
nodes {
id
title
repositories(first: 1) {
nodes {
url
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`;
const getProjectsV2FieldsGQL = `
query getProjectsV2Fields($id: ID!, $cursor: String) {
node(id: $id) {
... on ProjectV2 {
items(first: 10, after: $cursor) {
nodes {
content {
... on DraftIssue {
title
}
... on Issue {
title
}
}
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field {
... on ProjectV2SingleSelectField {
name
}
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`;
async function getGitHubProjectsV2(octokit: Octokit) {
const projects: Array<{ id: string, title: string, url: string | null }> = [];
const data = await octokit.graphql.paginate(getProjectsV2GQL, {
orgName: orgName
});
for (const node of data.organization.projectsV2.nodes) {
projects.push({
id: node.id,
title: node.title,
url: node.repositories.nodes[0]?.url ?? null,
});
}
return projects;
}
async function getGitHubProjectsV2Fields(octokit: Octokit, id: string) {
const output: Array<{ title: string, column: string }> = [];
const data: any = await octokit.graphql.paginate(getProjectsV2FieldsGQL, {
id: id
});
for (const node of data.node.items.nodes) {
output.push({
title: node.content.title,
column: node.fieldValues.nodes.find((fieldValue: any) => fieldValue.field?.name === 'Status')?.name
});
}
return output;
}
async function getGithubProjectsData(octokit: Octokit): Promise<GithubProjectResponse> {
const projects = await getGitHubProjectsV2(octokit);
const output: GithubProject[] = [];
for (const project of projects) {
if (!project.url) {
continue;
}
const projectOutput: GithubProject = {
title: project.title,
url: project.url,
tasks: [],
};
const fields = await getGitHubProjectsV2Fields(octokit, project.id);
const fieldMap: Record<string, GithubProjectTaskStatus> = {
done: 'completed',
in_progress: 'inprogress',
todo: 'notstarted',
}
for (const field of fields) {
const normalizedStatus = field.column.toLowerCase().replace(' ', '_');
const status = fieldMap[normalizedStatus];
if (!status) continue;
projectOutput.tasks.push({
status,
title: field.title,
})
}
output.push(projectOutput);
}
return {
projects: output,
};
}
export async function getGithubProjects(octokit: Octokit | null, ignoreCache = false): Promise<GithubProjectResponse> {
if (!cache || new Date(cache.createdAt.getTime() + cacheMaxAgeMs) < new Date() || ignoreCache) {
// No github credentials, assume there are no projects
if (!octokit) {
return {
projects: [],
}
}
cache = {
createdAt: new Date(),
response: await getGithubProjectsData(octokit)
};
}
return cache.response;
}

View File

@@ -0,0 +1,59 @@
import Stripe from "stripe";
export type StripeDonationResponse = {
donatorCount: number;
totalDonationsCents: number;
}
const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour
let cache: { response: StripeDonationResponse, createdAt: Date } | null = null;
async function getStripeDonationData(stripe: Stripe): Promise<StripeDonationResponse> {
const donationData: StripeDonationResponse = {
donatorCount: 0,
totalDonationsCents: 0,
};
let hasMore: boolean;
let lastId: string | null = null;
do {
const { data: activeSubscriptions, has_more } = await stripe.subscriptions.list({
limit: 100,
status: 'active',
starting_after: lastId ?? undefined,
});
for (const subscription of activeSubscriptions) {
const plan = (subscription as Stripe.Subscription & { plan?: Stripe.Plan }).plan;
donationData.donatorCount += 1;
donationData.totalDonationsCents += plan?.amount ?? 0;
lastId = subscription.id;
}
hasMore = has_more;
} while (hasMore);
return donationData;
}
export async function getStripeDonations(stripe: Stripe | null, ignoreCache = false): Promise<StripeDonationResponse> {
if (!cache || new Date(cache.createdAt.getTime() + cacheMaxAgeMs) < new Date() || ignoreCache) {
// No credentials, fill in blank data
if (!stripe) {
return {
donatorCount: 0,
totalDonationsCents: 0,
}
}
cache = {
createdAt: new Date(),
response: await getStripeDonationData(stripe)
};
}
return cache.response;
}

13
server/utils/octokit.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Octokit } from "octokit"
let octokit: Octokit | null = null;
export function useOctokit(event: H3Event): Octokit | null {
if (!octokit) {
const config = useRuntimeConfig(event);
if (config.githubApiToken) {
octokit = new Octokit({ auth: config.githubApiToken });
}
}
return octokit;
}

13
server/utils/stripe.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Stripe } from "stripe"
let stripe: Stripe | null = null;
export function useStripe(event: H3Event): Stripe | null {
if (!stripe) {
const config = useRuntimeConfig(event);
if (config.stripeSecretKey) {
stripe = new Stripe(config.stripeSecretKey);
}
}
return stripe;
}