Add CA admin UI and extend admin API for the unified platform.

Deliver parsers, events, analytics, and PI management in Vue; fix Telegram session mount and map navigation to events by eventId.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 14:40:59 +03:00
co-authored by Cursor
parent 9dfe713668
commit 2c16bc0b56
26 changed files with 2550 additions and 396 deletions
+120
View File
@@ -0,0 +1,120 @@
import { ADMIN_BASE, request } from "./client";
import type {
AnalyticsSummary,
Consumer,
ConsumerCreate,
ConsumerUpdate,
EventFilters,
EventListResponse,
EventRecord,
ParseJob,
ParseJobCreate,
TimelinePoint,
TopItem,
} from "../types/admin";
function buildQuery(params: Record<string, string | number | undefined>): string {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== "") {
search.set(key, String(value));
}
}
const qs = search.toString();
return qs ? `?${qs}` : "";
}
export function fetchJobs(): Promise<ParseJob[]> {
return request<ParseJob[]>("/jobs", undefined, ADMIN_BASE);
}
export function createJob(payload: ParseJobCreate): Promise<ParseJob> {
return request<ParseJob>(
"/jobs",
{ method: "POST", body: JSON.stringify(payload) },
ADMIN_BASE,
);
}
export function retryJob(jobId: number): Promise<ParseJob> {
return request<ParseJob>(
`/jobs/${jobId}/retry`,
{ method: "POST" },
ADMIN_BASE,
);
}
export function fetchEvents(filters: EventFilters = {}): Promise<EventListResponse> {
return request<EventListResponse>(
`/events${buildQuery(filters as Record<string, string | number | undefined>)}`,
undefined,
ADMIN_BASE,
);
}
export function fetchConsumers(): Promise<Consumer[]> {
return request<Consumer[]>("/consumers", undefined, ADMIN_BASE);
}
export function createConsumer(payload: ConsumerCreate): Promise<Consumer> {
return request<Consumer>(
"/consumers",
{ method: "POST", body: JSON.stringify(payload) },
ADMIN_BASE,
);
}
export function updateConsumer(id: number, payload: ConsumerUpdate): Promise<Consumer> {
return request<Consumer>(
`/consumers/${id}`,
{ method: "PATCH", body: JSON.stringify(payload) },
ADMIN_BASE,
);
}
export function rotateConsumerKey(id: number): Promise<Consumer> {
return request<Consumer>(
`/consumers/${id}/rotate-key`,
{ method: "POST" },
ADMIN_BASE,
);
}
export function fetchAnalyticsSummary(): Promise<AnalyticsSummary> {
return request<AnalyticsSummary>("/analytics/summary", undefined, ADMIN_BASE);
}
export function fetchAnalyticsTimeline(days = 30): Promise<TimelinePoint[]> {
return request<TimelinePoint[]>(
`/analytics/timeline${buildQuery({ days })}`,
undefined,
ADMIN_BASE,
);
}
export function fetchTopLocalities(limit = 10): Promise<TopItem[]> {
return request<TopItem[]>(
`/analytics/top-localities${buildQuery({ limit })}`,
undefined,
ADMIN_BASE,
);
}
export function fetchTopRegions(limit = 10): Promise<TopItem[]> {
return request<TopItem[]>(
`/analytics/top-regions${buildQuery({ limit })}`,
undefined,
ADMIN_BASE,
);
}
export async function testDistribution(apiKey: string, limit = 5): Promise<EventRecord[]> {
const response = await fetch(`/api/v1/events?limit=${limit}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Ошибка запроса: ${response.status}`);
}
return response.json() as Promise<EventRecord[]>;
}
@@ -0,0 +1,33 @@
const API_BASE = "/api";
const ADMIN_BASE = "/admin";
export async function request<T>(
url: string,
options?: RequestInit,
base: string = API_BASE,
): Promise<T> {
const headers = new Headers(options?.headers);
const isFormData = options?.body instanceof FormData;
if (!isFormData && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const response = await fetch(`${base}${url}`, {
...options,
headers,
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Ошибка запроса: ${response.status}`);
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
export { API_BASE, ADMIN_BASE };
+1 -27
View File
@@ -1,32 +1,6 @@
import { request } from "./client";
import type { MapObject, MapObjectCreate, MapObjectUpdate, ObjectMedia } from "../types/object";
const API_BASE = "/api";
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const headers = new Headers(options?.headers);
const isFormData = options?.body instanceof FormData;
if (!isFormData && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const response = await fetch(`${API_BASE}${url}`, {
...options,
headers,
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Ошибка запроса: ${response.status}`);
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
export function fetchObjects(): Promise<MapObject[]> {
return request<MapObject[]>("/objects");
}