130 lines
3.1 KiB
TypeScript
130 lines
3.1 KiB
TypeScript
export type Price = {
|
|
amount: number;
|
|
currency: string;
|
|
model: string;
|
|
};
|
|
|
|
export type ListingStats = {
|
|
installs?: number;
|
|
proposals_shown?: number;
|
|
proposals_accepted?: number;
|
|
};
|
|
|
|
export type ListingSummary = {
|
|
listing_id: string;
|
|
name: string;
|
|
summary: string;
|
|
category?: string;
|
|
price: Price;
|
|
seller: string;
|
|
stats?: ListingStats;
|
|
score?: number;
|
|
};
|
|
|
|
export type ListingDetail = ListingSummary & {
|
|
evidence_summary: string;
|
|
permissions: string[];
|
|
install_ref?: string;
|
|
install_command: string;
|
|
solution_version?: string;
|
|
version?: string;
|
|
};
|
|
|
|
export type Transaction = {
|
|
ts: string;
|
|
from: string;
|
|
to: string;
|
|
amount: number;
|
|
reason: string;
|
|
ref?: string;
|
|
};
|
|
|
|
export type Wallet = {
|
|
operator_id: string;
|
|
balance: number;
|
|
as_of?: string;
|
|
transactions: Transaction[];
|
|
};
|
|
|
|
export type Economy = {
|
|
operators: Array<{ operator_id: string; balance: number }>;
|
|
audit?: { date: string; url: string };
|
|
};
|
|
|
|
export type Governance = {
|
|
open_prs: Array<{ number: number; title: string; age_days: number; labels: string[]; url: string }>;
|
|
listings: Array<{ listing_id: string; status: string; url: string }>;
|
|
};
|
|
|
|
export type PanelName = 'search' | 'listing' | 'wallet' | 'economy' | 'governance' | 'login';
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
panel?: string;
|
|
|
|
constructor(message: string, status: number, panel?: string) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
this.panel = panel;
|
|
}
|
|
}
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const response = await fetch(path, {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(init?.headers ?? {}),
|
|
},
|
|
...init,
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
if (window.location.hash !== '#/login') {
|
|
window.location.hash = '#/login';
|
|
}
|
|
throw new ApiError('Login required', 401);
|
|
}
|
|
|
|
if (response.status === 502) {
|
|
const body = (await response.json().catch(() => null)) as { error?: string; panel?: string } | null;
|
|
throw new ApiError(body?.error ?? 'Panel is temporarily unavailable', 502, body?.panel);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => null)) as { error?: string } | null;
|
|
throw new ApiError(body?.error ?? `Request failed (${response.status})`, response.status);
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
export const api = {
|
|
login(passcode: string) {
|
|
return request<void>('/api/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ passcode }),
|
|
});
|
|
},
|
|
search(q: string, limit = 10) {
|
|
const params = new URLSearchParams({ q, limit: String(limit) });
|
|
return request<ListingSummary[]>(`/api/search?${params}`);
|
|
},
|
|
listing(listingId: string) {
|
|
return request<ListingDetail>(`/api/listing/${encodeURIComponent(listingId)}`);
|
|
},
|
|
wallet(operatorId: string) {
|
|
return request<Wallet>(`/api/wallet/${encodeURIComponent(operatorId)}`);
|
|
},
|
|
economy() {
|
|
return request<Economy>('/api/economy');
|
|
},
|
|
governance() {
|
|
return request<Governance>('/api/governance');
|
|
},
|
|
};
|