ui: tighten dashboard copy and fix layout issues

This commit is contained in:
k
2026-06-14 21:58:56 +08:00
parent 4c3c8c6f87
commit 8534d90589
19 changed files with 1036 additions and 345 deletions
+23 -18
View File
@@ -36,6 +36,19 @@ async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
return res.json() as Promise<T>
}
/** Signed-read credentials as headers (C3): keeps EIP-191 signatures out of
* URLs, access logs, and browser history. Backend still accepts the legacy
* ?ts=&sig= query params for old clients. */
function signedReadInit(env: SignedEnvelope): RequestInit {
return {
headers: {
'Content-Type': 'application/json',
'X-Sig-Ts': String(env.timestamp),
'X-Sig-Sig': env.signature,
},
}
}
export interface PostListResponse {
items: TrumpPost[]
total: number
@@ -104,22 +117,16 @@ export async function getTrades(
limit = 20,
page = 1,
): Promise<BotTrade[]> {
const qs = [
`wallet=${wallet.toLowerCase()}`,
`ts=${env.timestamp}`,
`sig=${encodeURIComponent(env.signature)}`,
`limit=${limit}`,
`page=${page}`,
].join('&')
return fetchJson<BotTrade[]>(`/trades?${qs}`)
const qs = `wallet=${wallet.toLowerCase()}&limit=${limit}&page=${page}`
return fetchJson<BotTrade[]>(`/trades?${qs}`, signedReadInit(env))
}
export async function getPerformance(
wallet: string,
env: SignedEnvelope,
): Promise<BotPerformance> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<BotPerformance>(`/performance?${qs}`)
const qs = `wallet=${wallet.toLowerCase()}`
return fetchJson<BotPerformance>(`/performance?${qs}`, signedReadInit(env))
}
export async function subscribe(
@@ -272,16 +279,16 @@ export async function getOpenPositions(
wallet: string,
env: SignedEnvelope,
): Promise<OpenPositionsResponse> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<OpenPositionsResponse>(`/positions/open?${qs}`)
const qs = `wallet=${wallet.toLowerCase()}`
return fetchJson<OpenPositionsResponse>(`/positions/open?${qs}`, signedReadInit(env))
}
export async function getTodayStats(
wallet: string,
env: SignedEnvelope,
): Promise<TodayStats> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<TodayStats>(`/positions/today?${qs}`)
const qs = `wallet=${wallet.toLowerCase()}`
return fetchJson<TodayStats>(`/positions/today?${qs}`, signedReadInit(env))
}
// ─── Scanner control (module 2) ─────────────────────────────────────────────
@@ -378,8 +385,7 @@ export async function getUser(
wallet: string,
env: SignedEnvelope,
): Promise<UserData> {
const qs = `ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<UserData>(`/user/${wallet.toLowerCase()}?${qs}`)
return fetchJson<UserData>(`/user/${wallet.toLowerCase()}`, signedReadInit(env))
}
export interface WindowAccuracy {
@@ -586,8 +592,7 @@ export async function getTelegramStatus(wallet: string, env?: SignedEnvelope): P
// Pass signature when available so the backend returns full binding details.
// Without it the endpoint returns only `bound: boolean` to prevent third-party de-anonymisation.
if (env) {
const qs = new URLSearchParams({ timestamp: String(env.timestamp), signature: env.signature })
return fetchJson<TelegramStatus>(`/telegram/${wallet}/status?${qs}`)
return fetchJson<TelegramStatus>(`/telegram/${wallet}/status`, signedReadInit(env))
}
return fetchJson<TelegramStatus>(`/telegram/${wallet}/status`)
}
+16 -5
View File
@@ -18,6 +18,11 @@ interface CacheEntry<T> {
data: T
ts: number
revalidating: boolean
/** onUpdate callbacks of every caller that hit this key while a background
* revalidation was in flight (M9 fix). All are notified when the fetch
* resolves — previously only the caller that STARTED the revalidation got
* fresh data; everyone else silently kept the stale copy. */
subscribers: Array<(fresh: T) => void>
}
const store = new Map<string, CacheEntry<unknown>>()
@@ -37,16 +42,22 @@ export async function swrFetch<T>(
// Still fresh — return immediately.
return hit.data
}
// Stale — return immediately AND revalidate in background.
// Stale — return immediately AND revalidate in background. Every caller's
// onUpdate joins the subscriber list; the in-flight fetch notifies all.
if (onUpdate) hit.subscribers.push(onUpdate)
if (!hit.revalidating) {
hit.revalidating = true
fetcher()
.then(fresh => {
store.set(key, { data: fresh, ts: Date.now(), revalidating: false })
onUpdate?.(fresh)
const subs = hit.subscribers
store.set(key, { data: fresh, ts: Date.now(), revalidating: false, subscribers: [] })
for (const notify of subs) {
try { notify(fresh) } catch { /* one bad subscriber must not starve the rest */ }
}
})
.catch(() => {
if (hit) hit.revalidating = false
hit.revalidating = false
hit.subscribers = []
})
}
return hit.data
@@ -54,7 +65,7 @@ export async function swrFetch<T>(
// Cache miss — must wait for the first fetch.
const data = await fetcher()
store.set(key, { data, ts: Date.now(), revalidating: false })
store.set(key, { data, ts: Date.now(), revalidating: false, subscribers: [] })
return data
}