Admin POST/PUT + admin.cms.write/审计;C 端 GET /home/banners;首页投影回退静态 homeFeeds;migration 000051;不碰 FeedSlot/支付/UGC。 Co-authored-by: Cursor <cursoragent@cursor.com>
691 lines
20 KiB
TypeScript
691 lines
20 KiB
TypeScript
/** Thin admin API client → /api/v1/admin (proxied to Go). */
|
||
|
||
export type ApiEnvelope<T> = { code: number; message: string; data?: T }
|
||
|
||
const TOKEN_KEY = 'yuxingu_admin_token'
|
||
|
||
/** Under /psy/admin use /psy/api; local vite uses /api. */
|
||
function adminAPIBase(): string {
|
||
if (typeof location !== 'undefined' && location.pathname.startsWith('/psy/admin')) {
|
||
return '/psy/api/v1/admin'
|
||
}
|
||
return '/api/v1/admin'
|
||
}
|
||
|
||
export function getToken(): string | null {
|
||
return localStorage.getItem(TOKEN_KEY)
|
||
}
|
||
|
||
export function setToken(token: string | null) {
|
||
if (token) localStorage.setItem(TOKEN_KEY, token)
|
||
else localStorage.removeItem(TOKEN_KEY)
|
||
}
|
||
|
||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||
const headers: Record<string, string> = { Accept: 'application/json' }
|
||
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
||
const token = getToken()
|
||
if (token) headers.Authorization = `Bearer ${token}`
|
||
const res = await fetch(`${adminAPIBase()}${path}`, {
|
||
method,
|
||
headers,
|
||
body: body === undefined ? undefined : JSON.stringify(body),
|
||
})
|
||
const text = await res.text()
|
||
let env: ApiEnvelope<T>
|
||
try {
|
||
env = JSON.parse(text) as ApiEnvelope<T>
|
||
} catch {
|
||
throw new Error(
|
||
res.status === 404
|
||
? `接口不存在或后端未更新(${path})`
|
||
: `响应不是 JSON(HTTP ${res.status})`,
|
||
)
|
||
}
|
||
if (!res.ok || env.code !== 0) {
|
||
throw new Error(env.message || `HTTP ${res.status}`)
|
||
}
|
||
return env.data as T
|
||
}
|
||
|
||
export type DashboardStats = {
|
||
users_total: number
|
||
membership_active: number
|
||
orders_today: number
|
||
paid_cents_today: number
|
||
ask_replies_today: number
|
||
profiles_total: number
|
||
reports_total: number
|
||
series?: Array<{
|
||
day: string
|
||
new_users: number
|
||
orders: number
|
||
paid_cents: number
|
||
ask_replies: number
|
||
}>
|
||
reports_by_type?: Array<{ type: string; count: number }>
|
||
}
|
||
|
||
export type UserListItem = {
|
||
id: string
|
||
status: string
|
||
phone?: string | null
|
||
nickname?: string | null
|
||
ask_paid_quota_left: number
|
||
profile_count: number
|
||
membership_active: boolean
|
||
membership_plan?: string | null
|
||
created_at: string
|
||
}
|
||
|
||
export type UserDetail = {
|
||
id: string
|
||
status: string
|
||
phone?: string | null
|
||
nickname?: string | null
|
||
ask_paid_quota_left: number
|
||
created_at: string
|
||
profiles: Array<{ id: string; relation: string; display_name: string; birth_date?: string }>
|
||
reports: Array<{ id: string; type: string; created_at: string }>
|
||
membership: {
|
||
active: boolean
|
||
plan?: string
|
||
status: string
|
||
expires_at?: string | null
|
||
ask_quota_left?: number
|
||
}
|
||
recent_orders: Array<{
|
||
id: string
|
||
kind: string
|
||
plan?: string
|
||
status: string
|
||
amount_cents: number
|
||
created_at: string
|
||
}>
|
||
}
|
||
|
||
export type AdminRole = 'super' | 'ops'
|
||
|
||
export type AdminMe = {
|
||
id: string
|
||
username: string
|
||
role?: string
|
||
permissions?: string[]
|
||
}
|
||
|
||
export type PushJob = {
|
||
id: string
|
||
title: string
|
||
body: string
|
||
audience: string
|
||
status: 'draft' | 'cancelled'
|
||
created_by: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export type AdminListItem = {
|
||
id: string
|
||
username: string
|
||
role: AdminRole
|
||
status: string
|
||
created_at: string
|
||
}
|
||
|
||
export const adminApi = {
|
||
login: (username: string, password: string) =>
|
||
request<{ token: string; admin: AdminMe }>('POST', '/auth/login', {
|
||
username,
|
||
password,
|
||
}),
|
||
logout: () => request<{ ok: boolean }>('POST', '/auth/logout'),
|
||
me: () => request<AdminMe>('GET', '/me'),
|
||
roles: () =>
|
||
request<{ items: Array<{ id: string; name: string; system: boolean }> }>('GET', '/roles'),
|
||
role: (id: string) =>
|
||
request<{ id: string; name: string; system: boolean; permissions: string[] }>('GET', `/roles/${id}`),
|
||
stats: () => request<DashboardStats>('GET', '/stats'),
|
||
users: (q = '') =>
|
||
request<{ items: UserListItem[] }>('GET', `/users?q=${encodeURIComponent(q)}`),
|
||
user: (id: string) => request<UserDetail>('GET', `/users/${id}`),
|
||
setUserStatus: (id: string, status: string, reason: string) =>
|
||
request<UserDetail>('POST', `/users/${id}/status`, { status, reason }),
|
||
statusTransitions: (id: string) =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
from_status: string
|
||
to_status: string
|
||
admin_id: string
|
||
reason: string
|
||
created_at: string
|
||
}>
|
||
}>('GET', `/users/${id}/status-transitions`),
|
||
userInsight: (id: string) =>
|
||
request<{
|
||
user_id: string
|
||
profiles_count: number
|
||
reports_by_type: Array<{ type: string; count: number }>
|
||
recent_reports: Array<{ id: string; type: string; created_at: string }>
|
||
tags: Array<{ code: string; label: string }>
|
||
behavior: {
|
||
events: Array<{ name: string; page_path?: string; received_at: string }>
|
||
ask_thread_count: number
|
||
}
|
||
}>('GET', `/users/${id}/insight`),
|
||
userEntitlements: (id: string) =>
|
||
request<{
|
||
user_id: string
|
||
membership: {
|
||
plan?: string
|
||
status: string
|
||
expires_at?: string
|
||
ask_quota_left?: number
|
||
active: boolean
|
||
}
|
||
ask_paid_quota_left: number
|
||
flags: {
|
||
report_detail_via_membership: boolean
|
||
deep_access_count: number
|
||
}
|
||
deep_accesses: Array<{
|
||
id: string
|
||
report_id: string
|
||
report_type?: string
|
||
created_at: string
|
||
}>
|
||
}>('GET', `/users/${id}/entitlements`),
|
||
grant: (id: string, plan: string) =>
|
||
request<{ ok: boolean }>('POST', `/users/${id}/membership/grant`, { plan }),
|
||
grantAskQuota: (id: string, delta: number) =>
|
||
request<{ ok: boolean; ask_paid_quota_left: number }>('POST', `/users/${id}/ask-quota/grant`, {
|
||
delta,
|
||
}),
|
||
admins: () => request<{ items: AdminListItem[] }>('GET', '/admins'),
|
||
patchAdminRole: (id: string, role: AdminRole) =>
|
||
request<{ ok: boolean }>('PATCH', `/admins/${id}`, { role }),
|
||
pushJobs: () => request<{ items: PushJob[] }>('GET', '/push-jobs'),
|
||
createPushJob: (body: { title: string; body?: string; audience?: string }) =>
|
||
request<PushJob>('POST', '/push-jobs', body),
|
||
patchPushJob: (id: string, body: { title?: string; body?: string; status?: 'draft' | 'cancelled' }) =>
|
||
request<PushJob>('PATCH', `/push-jobs/${id}`, body),
|
||
membershipPlans: () =>
|
||
request<{
|
||
items: Array<{
|
||
code: string
|
||
title: string
|
||
duration_days: number
|
||
amount_cents: number
|
||
active: boolean
|
||
updated_at: string
|
||
}>
|
||
}>('GET', '/membership-plans'),
|
||
updateMembershipPlan: (
|
||
code: string,
|
||
body: { title: string; duration_days: number; amount_cents: number; active: boolean },
|
||
) =>
|
||
request<{
|
||
code: string
|
||
title: string
|
||
duration_days: number
|
||
amount_cents: number
|
||
active: boolean
|
||
}>('PUT', `/membership-plans/${code}`, body),
|
||
createRedemptionBatch: (body: { label: string; plan_code: string; quantity: number }) =>
|
||
request<{
|
||
batch: { id: string; label: string; plan_code: string; quantity: number }
|
||
codes: Array<{ id: string; code: string; status: string }>
|
||
}>('POST', '/redemption-batches', body),
|
||
redemptionBatches: () =>
|
||
request<{ items: Array<{ id: string; label: string; plan_code: string; quantity: number; created_at: string }> }>(
|
||
'GET',
|
||
'/redemption-batches',
|
||
),
|
||
redemptionCodes: (batchId: string) =>
|
||
request<{ items: Array<{ id: string; code: string; status: string; plan_code: string }> }>(
|
||
'GET',
|
||
`/redemption-batches/${batchId}/codes`,
|
||
),
|
||
disableRedemptionCode: (id: string) => request<{ ok: boolean }>('POST', `/redemption-codes/${id}/disable`),
|
||
askThreads: (userId = '') => {
|
||
const q = userId ? `?user_id=${encodeURIComponent(userId)}` : ''
|
||
return request<{
|
||
items: Array<{
|
||
id: string
|
||
user_id: string
|
||
profile_id: string
|
||
scene?: string
|
||
message_count: number
|
||
created_at: string
|
||
updated_at: string
|
||
}>
|
||
}>('GET', `/ask/threads${q}`)
|
||
},
|
||
askThread: (id: string) =>
|
||
request<{
|
||
id: string
|
||
user_id: string
|
||
profile_id: string
|
||
scene?: string
|
||
message_count: number
|
||
created_at: string
|
||
updated_at: string
|
||
}>('GET', `/ask/threads/${id}`),
|
||
askThreadMessages: (id: string) =>
|
||
request<{
|
||
id: string
|
||
user_id: string
|
||
profile_id: string
|
||
scene?: string
|
||
message_count: number
|
||
created_at: string
|
||
updated_at: string
|
||
messages: Array<{ id: string; role: string; content: string; created_at: string }>
|
||
}>('GET', `/ask/threads/${id}/messages`),
|
||
filterRules: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
category: string
|
||
pattern: string
|
||
action: string
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>
|
||
}>('GET', '/content-safety/filter-rules'),
|
||
filterRule: (id: string) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
category: string
|
||
pattern: string
|
||
action: string
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('GET', `/content-safety/filter-rules/${id}`),
|
||
evaluateContent: (text: string) =>
|
||
request<{ matches: Array<{ code: string; title: string; category: string; action: string }> }>(
|
||
'POST',
|
||
'/content-safety/evaluate',
|
||
{ text },
|
||
),
|
||
askFeedback: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
thread_id: string
|
||
message_id?: string
|
||
source: string
|
||
rating: number
|
||
tag?: string
|
||
note?: string
|
||
created_at: string
|
||
}>
|
||
}>('GET', '/ask/feedback'),
|
||
createAskFeedback: (threadId: string, body: { rating: number; tag?: string; note?: string; message_id?: string }) =>
|
||
request<{
|
||
id: string
|
||
thread_id: string
|
||
rating: number
|
||
source: string
|
||
}>('POST', `/ask/threads/${threadId}/feedback`, body),
|
||
systemPrompts: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
scene?: string
|
||
body: string
|
||
version: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>
|
||
}>('GET', '/ai/system-prompts'),
|
||
systemPrompt: (id: string) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
scene?: string
|
||
body: string
|
||
version: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('GET', `/ai/system-prompts/${id}`),
|
||
knowledgeSources: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
description?: string
|
||
source_kind: string
|
||
version: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>
|
||
}>('GET', '/ai/knowledge-sources'),
|
||
knowledgeSource: (id: string) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
description?: string
|
||
source_kind: string
|
||
version: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('GET', `/ai/knowledge-sources/${id}`),
|
||
crisisPolicies: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
severity: string
|
||
pattern: string
|
||
action: string
|
||
helpline_text?: string
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>
|
||
}>('GET', '/crisis/policies'),
|
||
crisisPolicy: (id: string) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
severity: string
|
||
pattern: string
|
||
action: string
|
||
helpline_text?: string
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('GET', `/crisis/policies/${id}`),
|
||
evaluateCrisis: (text: string) =>
|
||
request<{
|
||
matches: Array<{
|
||
code: string
|
||
title: string
|
||
severity: string
|
||
action: string
|
||
helpline_text?: string
|
||
}>
|
||
}>('POST', '/crisis/evaluate', { text }),
|
||
banners: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
placement: string
|
||
image_url?: string
|
||
link_path?: string
|
||
sort_order: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>
|
||
}>('GET', '/cms/banners'),
|
||
banner: (id: string) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
placement: string
|
||
image_url?: string
|
||
link_path?: string
|
||
sort_order: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('GET', `/cms/banners/${id}`),
|
||
createBanner: (body: {
|
||
code: string
|
||
title: string
|
||
placement: string
|
||
image_url?: string | null
|
||
link_path?: string | null
|
||
sort_order: number
|
||
active: boolean
|
||
}) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
placement: string
|
||
image_url?: string
|
||
link_path?: string
|
||
sort_order: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('POST', '/cms/banners', body),
|
||
updateBanner: (
|
||
id: string,
|
||
body: {
|
||
code: string
|
||
title: string
|
||
placement: string
|
||
image_url?: string | null
|
||
link_path?: string | null
|
||
sort_order: number
|
||
active: boolean
|
||
},
|
||
) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
placement: string
|
||
image_url?: string
|
||
link_path?: string
|
||
sort_order: number
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('PUT', `/cms/banners/${id}`, body),
|
||
feedSlots: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
slot_key: string
|
||
placement: string
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>
|
||
}>('GET', '/cms/feed-slots'),
|
||
feedSlot: (id: string) =>
|
||
request<{
|
||
id: string
|
||
code: string
|
||
title: string
|
||
slot_key: string
|
||
placement: string
|
||
active: boolean
|
||
system: boolean
|
||
updated_at: string
|
||
}>('GET', `/cms/feed-slots/${id}`),
|
||
publications: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/cms/publications'),
|
||
publication: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/cms/publications/${id}`),
|
||
knowledgeChunks: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/ai/knowledge-chunks'),
|
||
knowledgeChunk: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/ai/knowledge-chunks/${id}`),
|
||
tools: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/ai/tools'),
|
||
tool: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/ai/tools/${id}`),
|
||
blockPolicies: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/content-safety/block-policies'),
|
||
blockPolicy: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/content-safety/block-policies/${id}`),
|
||
cases: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/content-safety/cases'),
|
||
case: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/content-safety/cases/${id}`),
|
||
events: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/crisis/events'),
|
||
event: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/crisis/events/${id}`),
|
||
interventions: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/crisis/interventions'),
|
||
intervention: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/crisis/interventions/${id}`),
|
||
handoffs: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/ask/handoffs'),
|
||
handoff: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/ask/handoffs/${id}`),
|
||
requests: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/privacy/requests'),
|
||
privacyRequest: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/privacy/requests/${id}`),
|
||
starConfigs: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/star-configs'),
|
||
starConfig: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/explore/star-configs/${id}`),
|
||
rhythmConfigs: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/rhythm-configs'),
|
||
rhythmConfig: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/explore/rhythm-configs/${id}`),
|
||
imageCardDecks: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/image-card-decks'),
|
||
imageCardDeck: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/explore/image-card-decks/${id}`),
|
||
reportTemplates: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/growth/report-templates'),
|
||
reportTemplate: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/growth/report-templates/${id}`),
|
||
funnelDefinitions: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/analytics/funnel-definitions'),
|
||
funnelDefinition: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/analytics/funnel-definitions/${id}`),
|
||
exploreScales: () =>
|
||
request<{ items: Array<Record<string, unknown>> }>('GET', '/explore/scales'),
|
||
exploreScale: (id: string) =>
|
||
request<Record<string, unknown>>('GET', `/explore/scales/${id}`),
|
||
orders: (params?: {
|
||
status?: string
|
||
kind?: string
|
||
from?: string
|
||
to?: string
|
||
user_id?: string
|
||
limit?: number
|
||
offset?: number
|
||
}) => {
|
||
const q = new URLSearchParams()
|
||
if (params?.status) q.set('status', params.status)
|
||
if (params?.kind) q.set('kind', params.kind)
|
||
if (params?.from) q.set('from', params.from)
|
||
if (params?.to) q.set('to', params.to)
|
||
if (params?.user_id) q.set('user_id', params.user_id)
|
||
if (params?.limit != null) q.set('limit', String(params.limit))
|
||
if (params?.offset != null) q.set('offset', String(params.offset))
|
||
const qs = q.toString()
|
||
return request<{
|
||
items: Array<{
|
||
id: string
|
||
user_id: string
|
||
kind: string
|
||
plan?: string
|
||
amount_cents: number
|
||
status: string
|
||
refund_status: string
|
||
created_at: string
|
||
}>
|
||
}>('GET', `/orders${qs ? `?${qs}` : ''}`)
|
||
},
|
||
planPrices: () =>
|
||
request<{ items: Array<{ plan: string; display_cents: number; updated_at: string }> }>(
|
||
'GET',
|
||
'/membership/plan-prices',
|
||
),
|
||
savePlanPrices: (items: Array<{ plan: string; display_cents: number }>) =>
|
||
request<{ ok: boolean }>('PUT', '/membership/plan-prices', { items }),
|
||
audit: () =>
|
||
request<{
|
||
items: Array<{
|
||
id: string
|
||
admin_id: string
|
||
action: string
|
||
target_type: string
|
||
target_id: string
|
||
meta: unknown
|
||
created_at: string
|
||
}>
|
||
}>('GET', '/audit-logs'),
|
||
analyticsOverview: (from: string, to: string) =>
|
||
request<AnalyticsOverview>('GET', `/analytics/overview?from=${from}&to=${to}`),
|
||
analyticsPages: (from: string, to: string) =>
|
||
request<{ items: AnalyticsPageRow[] }>('GET', `/analytics/pages?from=${from}&to=${to}`),
|
||
analyticsExits: (from: string, to: string) =>
|
||
request<{ items: AnalyticsExitRow[] }>('GET', `/analytics/exits?from=${from}&to=${to}`),
|
||
analyticsClicks: (from: string, to: string) =>
|
||
request<{ items: AnalyticsClickRow[] }>('GET', `/analytics/clicks?from=${from}&to=${to}`),
|
||
analyticsFunnel: (from: string, to: string) =>
|
||
request<{ steps: AnalyticsFunnelStep[] }>('GET', `/analytics/funnel?from=${from}&to=${to}`),
|
||
homeTools: () => request<{ items: HomeToolAdmin[] }>('GET', '/home/tools'),
|
||
saveHomeTools: (items: HomeToolAdmin[]) =>
|
||
request<{ ok: boolean }>('PUT', '/home/tools', { items }),
|
||
scales: () => request<{ items: ScaleAdmin[] }>('GET', '/scales'),
|
||
patchScale: (id: string, status: 'published' | 'draft') =>
|
||
request<{ ok: boolean }>('PATCH', `/scales/${id}`, { status }),
|
||
}
|
||
|
||
export type HomeToolAdmin = {
|
||
id?: string
|
||
row_index: number
|
||
sort_order: number
|
||
path: string
|
||
icon: string
|
||
label: string
|
||
badge?: string | null
|
||
badge_tone?: string | null
|
||
enabled: boolean
|
||
}
|
||
|
||
export type ScaleAdmin = {
|
||
id: string
|
||
slug: string
|
||
title: string
|
||
description: string
|
||
status: string
|
||
}
|
||
|
||
export type AnalyticsOverview = {
|
||
dau: number
|
||
new_users: number
|
||
sessions: number
|
||
avg_session_ms: number
|
||
series?: Array<{ day: string; dau: number; sessions: number }>
|
||
}
|
||
|
||
export type AnalyticsPageRow = {
|
||
page_path: string
|
||
pv: number
|
||
uv: number
|
||
avg_dwell_ms: number
|
||
exit_count: number
|
||
}
|
||
|
||
export type AnalyticsExitRow = { exit_page: string; count: number }
|
||
export type AnalyticsClickRow = { element_id: string; count: number }
|
||
export type AnalyticsFunnelStep = { name: string; count: number }
|