mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 05:12:16 +03:00
fix(dashboard): keep Profile XP and level consistent (#11604)
Merged via /merge-batch (lote 2026-08-26 batch 2, v3.8.51). Boarded no worktree combinado junto com outras ~20 PRs; validação única: typecheck/complexity/cognitive-complexity/changelog-integrity verdes, file-size rebaseado onde necessário (crescimento legítimo), lint com os mesmos 228 achados pré-existentes confirmados via sonda contra o tip puro (não introduzidos por este lote), e 292 testes focados (unit) + 18 (vitest) passando. Obrigado pela contribuição.
This commit is contained in:
@@ -299,22 +299,22 @@ export function getBadgeDefinitions(category?: string): BadgeDefinition[] {
|
||||
|
||||
/**
|
||||
* Aggregate XP across every API key (the operator-wide profile view used by the
|
||||
* dashboard profile page, which is not scoped to a single key). Sums total XP and
|
||||
* takes the highest reached level. (#3484)
|
||||
* dashboard profile page, which is not scoped to a single key). The aggregate
|
||||
* level must be derived from the same summed XP displayed by the profile. (#3484)
|
||||
*/
|
||||
export function getAggregateXp(): UserLevelRow {
|
||||
const row = db()
|
||||
.prepare(
|
||||
`SELECT COALESCE(SUM(total_xp), 0) AS total_xp,
|
||||
COALESCE(MAX(current_level), 1) AS current_level,
|
||||
MAX(updated_at) AS updated_at
|
||||
FROM user_levels`
|
||||
)
|
||||
.get() as { total_xp: number; current_level: number; updated_at: string | null };
|
||||
.get() as { total_xp: number; updated_at: string | null };
|
||||
const totalXp = row?.total_xp ?? 0;
|
||||
return {
|
||||
apiKeyId: "*",
|
||||
totalXp: row?.total_xp ?? 0,
|
||||
currentLevel: row?.current_level ?? 1,
|
||||
totalXp,
|
||||
currentLevel: calculateLevel(totalXp),
|
||||
updatedAt: row?.updated_at ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,22 +44,32 @@ export function cumulativeXpForLevel(level: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate level from total XP using the inverse of the cumulative XP curve.
|
||||
* Calculate the exact level from total XP.
|
||||
*
|
||||
* The cumulative XP for level L approximates to `100 * L^2.5 / 2.5`.
|
||||
* Solving for L gives `L ≈ (totalXp * 2.5 / 100) ^ 0.4`.
|
||||
* The inverse cumulative curve supplies a fast initial estimate. The result is
|
||||
* then reconciled against the exact floored thresholds from `xpForLevel`.
|
||||
*
|
||||
* @param totalXp - Total accumulated XP
|
||||
* @returns Current level (minimum 1)
|
||||
*
|
||||
* @example
|
||||
* calculateLevel(0) // 1
|
||||
* calculateLevel(5000) // ~8
|
||||
* calculateLevel(100000) // ~100
|
||||
* calculateLevel(cumulativeXpForLevel(10)) // 10
|
||||
*/
|
||||
export function calculateLevel(totalXp: number): number {
|
||||
if (totalXp <= 0) return 1;
|
||||
return Math.max(1, Math.floor(Math.pow((totalXp * 2.5) / 100, 0.4)));
|
||||
if (!Number.isFinite(totalXp) || totalXp <= 0) return 1;
|
||||
const boundedXp = Math.min(totalXp, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
// Use the inverse curve only as a fast starting point, then reconcile it
|
||||
// against the exact floored cumulative thresholds used by the XP engine.
|
||||
let level = Math.max(1, Math.floor(Math.pow((boundedXp * 2.5) / 100, 0.4)));
|
||||
while (level > 1 && cumulativeXpForLevel(level) > boundedXp) {
|
||||
level -= 1;
|
||||
}
|
||||
while (cumulativeXpForLevel(level + 1) <= boundedXp) {
|
||||
level += 1;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user