m2-market/web/frontend/src/api.ts

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');
},
};