Files
hackyguruandClaude Fable 5 2859562e60 Remove discussion threads from milestone cards
Milestone cards show tracking data only (status, payout, dates, reviewer,
deliverables, links). Review discussion happens on the proposal issue on
GitHub; the app no longer fetches comments or posts reviews — drops
ReviewComposer, postReviewComment, fetchMilestoneReviews, and the
MilestoneReview comment-parsing conventions. The PAT is now only needed
for filing feedback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:56:15 +01:00

90 lines
2.8 KiB
TypeScript

// Client-side GitHub access. The token lives only in the engineer's browser
// (localStorage) and is sent only to api.github.com — the dashboard itself
// stays a static site with no server-held secrets.
import { FEEDBACK_LABEL, FEEDBACK_REPO } from "./config";
import type { FeedbackIssue } from "./types";
const TOKEN_KEY = "rfp-tracker:github-token";
export const TOKEN_EVENT = "rfp-tracker:token-changed";
export function getStoredToken(): string | null {
if (typeof window === "undefined") return null;
return window.localStorage.getItem(TOKEN_KEY);
}
export function storeToken(token: string | null) {
if (token) window.localStorage.setItem(TOKEN_KEY, token);
else window.localStorage.removeItem(TOKEN_KEY);
window.dispatchEvent(new Event(TOKEN_EVENT));
}
async function gh(path: string, init: RequestInit = {}) {
const token = getStoredToken();
if (!token) throw new Error("No GitHub token connected");
const res = await fetch(`https://api.github.com${path}`, {
...init,
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
...(init.body ? { "Content-Type": "application/json" } : {}),
},
});
if (!res.ok) {
let detail = "";
try {
detail = ((await res.json()) as { message?: string }).message ?? "";
} catch {
/* non-JSON error body */
}
const hint =
res.status === 401
? "Token is invalid or expired"
: res.status === 403 || res.status === 404
? "Token lacks permission on this repository"
: `GitHub returned ${res.status}`;
throw new Error(detail ? `${hint}${detail}` : hint);
}
return res.status === 204 ? null : res.json();
}
export async function fetchViewer(): Promise<{ login: string }> {
return gh("/user") as Promise<{ login: string }>;
}
// Files community feedback as a `feedback`-labeled issue in FEEDBACK_REPO
// under the connected user's account. Labels only stick when the token's
// user has triage rights on the repo.
export async function createFeedbackIssue(
title: string,
body: string,
): Promise<FeedbackIssue> {
const it = (await gh(`/repos/${FEEDBACK_REPO}/issues`, {
method: "POST",
body: JSON.stringify({ title, body, labels: [FEEDBACK_LABEL] }),
})) as {
number: number;
title: string;
body?: string | null;
html_url: string;
state: string;
created_at: string;
comments: number;
user: { login: string };
labels: { name: string }[];
};
return {
repo: FEEDBACK_REPO,
number: it.number,
title: it.title,
body: it.body ?? "",
url: it.html_url,
state: it.state === "closed" ? "closed" : "open",
author: it.user.login,
created_at: it.created_at,
labels: it.labels.map((l) => l.name).filter((n) => n !== FEEDBACK_LABEL),
comments: it.comments,
};
}