Ship Steam login, D1 player sync, and cached「我」dashboard.

Players get a fast TTL-backed homepage (local profile / Cloudflare D1) with dense UI polish; login unlocks /home without blocking on every OpenDota refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-08-01 01:24:30 +08:00
co-authored by Cursor
parent 4a61aeeb26
commit f5b7011c45
65 changed files with 7304 additions and 552 deletions
+151
View File
@@ -0,0 +1,151 @@
/** D1 helpers for Pages Functions (subset of cloudflare/player-sync/src/db.js). */
export function utcNow() {
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
}
export function jsonResponse(body, status = 200, extra = {}) {
return new Response(JSON.stringify(body), {
status,
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
...extra,
},
});
}
export async function loadPlayerBundle(db, accountId) {
if (!db) return null;
const user = await db
.prepare(`SELECT * FROM users WHERE account_id = ?`)
.bind(accountId)
.first();
if (!user) return null;
const profile = await db
.prepare(`SELECT * FROM player_profiles WHERE account_id = ?`)
.bind(accountId)
.first();
const statsRows = await db
.prepare(`SELECT * FROM player_stats WHERE account_id = ?`)
.bind(accountId)
.all();
const heroes = await db
.prepare(
`SELECT * FROM player_heroes WHERE account_id = ? ORDER BY games DESC LIMIT 8`
)
.bind(accountId)
.all();
const peers = await db
.prepare(
`SELECT * FROM player_peers WHERE account_id = ? ORDER BY games DESC LIMIT 8`
)
.bind(accountId)
.all();
const recent = await db
.prepare(
`SELECT * FROM player_matches WHERE account_id = ? ORDER BY start_time DESC LIMIT 20`
)
.bind(accountId)
.all();
const statsByScope = {};
for (const row of (statsRows && statsRows.results) || []) {
try {
statsByScope[row.scope] = row.payload_json
? JSON.parse(row.payload_json)
: row;
} catch {
statsByScope[row.scope] = row;
}
}
return {
account_id: accountId,
personaname: user.personaname,
avatar: user.avatar,
public_share: !!user.public_share,
rank_tier: profile && profile.rank_tier,
leaderboard_rank: profile && profile.leaderboard_rank,
availability: profile
? {
status: profile.availability_status,
note: profile.availability_note,
complete: !!profile.availability_complete,
source: profile.source,
fetched_at: profile.fetched_at,
stale: false,
}
: null,
career: statsByScope.career || null,
recent_20: statsByScope.recent20 || null,
activity_180: statsByScope.recent180 || null,
top_heroes: ((heroes && heroes.results) || []).map((h) => ({
hero_id: h.hero_id,
hero_key: h.hero_key,
hero_name_loc: h.hero_name_loc,
games: h.games,
wins: h.wins,
winrate: h.winrate,
last_played: h.last_played,
})),
peers: ((peers && peers.results) || []).map((p) => ({
account_id: p.peer_account_id,
personaname: p.personaname,
avatar: p.avatar,
games: p.games,
wins: p.wins,
winrate: p.winrate,
})),
recent: ((recent && recent.results) || []).map((r) => ({
match_id: r.match_id,
start_time: r.start_time,
duration: r.duration,
won: !!r.won,
hero_id: r.hero_id,
hero_key: r.hero_key,
hero_name_loc: r.hero_name_loc,
kills: r.kills,
deaths: r.deaths,
assists: r.assists,
kda: r.kda,
gpm: r.gpm,
xpm: r.xpm,
hero_damage: r.hero_damage,
game_mode: r.game_mode,
lobby_type: r.lobby_type,
})),
updated_at: (profile && profile.updated_at) || user.last_login_at,
enriched_at: profile && profile.enriched_at,
};
}
export function isStale(fetchedAt, maxAgeMs = 10 * 60 * 1000) {
if (!fetchedAt) return true;
const t = Date.parse(fetchedAt);
if (!Number.isFinite(t)) return true;
return Date.now() - t > maxAgeMs;
}
export async function enqueueRefresh(env, payload) {
if (!env.SYNC_QUEUE) return false;
const jobId = crypto.randomUUID();
const now = utcNow();
if (env.DB) {
await env.DB.prepare(
`INSERT INTO sync_jobs (id, account_id, kind, match_id, status, attempts, created_at, updated_at)
VALUES (?, ?, ?, ?, 'queued', 0, ?, ?)`
)
.bind(
jobId,
payload.account_id,
payload.kind || "login_refresh",
payload.match_id || null,
now,
now
)
.run();
}
await env.SYNC_QUEUE.send({ ...payload, job_id: jobId });
return true;
}