fix(db): atomic saveMnemonic, const normalizedLetter, updateProfileSeen guard, ?? 0 for avgScore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+16
-15
@@ -73,7 +73,8 @@ function initDb(dbPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProfileSeen(id) {
|
function updateProfileSeen(id) {
|
||||||
instance.prepare('UPDATE profiles SET last_seen = ? WHERE id = ?').run(Date.now(), id)
|
const result = instance.prepare('UPDATE profiles SET last_seen = ? WHERE id = ?').run(Date.now(), id)
|
||||||
|
if (result.changes === 0) console.warn(`[db] updateProfileSeen: no profile id=${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMagicToken(token, profileId, expiresAt) {
|
function createMagicToken(token, profileId, expiresAt) {
|
||||||
@@ -128,7 +129,7 @@ function initDb(dbPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordAnswer(profileId, letter, correct) {
|
function recordAnswer(profileId, letter, correct) {
|
||||||
letter = letter.toUpperCase()
|
const normalizedLetter = letter.toUpperCase()
|
||||||
const progress = getOrCreateProgress(profileId)
|
const progress = getOrCreateProgress(profileId)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const isFirstToday = !progress.last_session_at ||
|
const isFirstToday = !progress.last_session_at ||
|
||||||
@@ -156,15 +157,15 @@ function initDb(dbPath) {
|
|||||||
// Update letter stats
|
// Update letter stats
|
||||||
const existing = instance
|
const existing = instance
|
||||||
.prepare('SELECT * FROM letter_stats WHERE profile_id = ? AND letter = ?')
|
.prepare('SELECT * FROM letter_stats WHERE profile_id = ? AND letter = ?')
|
||||||
.get(profileId, letter)
|
.get(profileId, normalizedLetter)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
instance.prepare(
|
instance.prepare(
|
||||||
'UPDATE letter_stats SET correct = correct + ?, attempts = attempts + 1, updated_at = ? WHERE profile_id = ? AND letter = ?'
|
'UPDATE letter_stats SET correct = correct + ?, attempts = attempts + 1, updated_at = ? WHERE profile_id = ? AND letter = ?'
|
||||||
).run(correct ? 1 : 0, now, profileId, letter)
|
).run(correct ? 1 : 0, now, profileId, normalizedLetter)
|
||||||
} else {
|
} else {
|
||||||
instance.prepare(
|
instance.prepare(
|
||||||
'INSERT INTO letter_stats (profile_id, letter, correct, attempts, updated_at) VALUES (?,?,?,1,?)'
|
'INSERT INTO letter_stats (profile_id, letter, correct, attempts, updated_at) VALUES (?,?,?,1,?)'
|
||||||
).run(profileId, letter, correct ? 1 : 0, now)
|
).run(profileId, normalizedLetter, correct ? 1 : 0, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance.prepare('SELECT * FROM user_progress WHERE profile_id = ?').get(profileId)
|
return instance.prepare('SELECT * FROM user_progress WHERE profile_id = ?').get(profileId)
|
||||||
@@ -181,17 +182,17 @@ function initDb(dbPath) {
|
|||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const upperLetter = letter.toUpperCase()
|
const upperLetter = letter.toUpperCase()
|
||||||
|
|
||||||
instance.prepare(
|
// Try insert first — changes=1 means new row (first save), changes=0 means update path
|
||||||
`INSERT INTO user_mnemonics (profile_id, letter, phrase, created_at) VALUES (?,?,?,?)
|
const insertResult = instance.prepare(
|
||||||
ON CONFLICT(profile_id, letter) DO UPDATE SET phrase = excluded.phrase`
|
'INSERT OR IGNORE INTO user_mnemonics (profile_id, letter, phrase, created_at) VALUES (?,?,?,?)'
|
||||||
).run(profileId, upperLetter, phrase, now)
|
).run(profileId, upperLetter, phrase, now)
|
||||||
|
|
||||||
// Only award 50 pts on first save — if created_at = now, it was just inserted
|
if (insertResult.changes === 0) {
|
||||||
const row = instance.prepare(
|
// Existing mnemonic — update phrase only, no point award
|
||||||
'SELECT created_at FROM user_mnemonics WHERE profile_id = ? AND letter = ?'
|
instance.prepare('UPDATE user_mnemonics SET phrase = ? WHERE profile_id = ? AND letter = ?')
|
||||||
).get(profileId, upperLetter)
|
.run(phrase, profileId, upperLetter)
|
||||||
|
} else {
|
||||||
if (row.created_at === now) {
|
// First save — award 50 points
|
||||||
getOrCreateProgress(profileId)
|
getOrCreateProgress(profileId)
|
||||||
instance.prepare('UPDATE user_progress SET score = score + 50, updated_at = ? WHERE profile_id = ?')
|
instance.prepare('UPDATE user_progress SET score = score + 50, updated_at = ? WHERE profile_id = ?')
|
||||||
.run(now, profileId)
|
.run(now, profileId)
|
||||||
@@ -218,7 +219,7 @@ function initDb(dbPath) {
|
|||||||
|
|
||||||
function getAdminStats() {
|
function getAdminStats() {
|
||||||
const totalUsers = instance.prepare('SELECT COUNT(*) as c FROM profiles').get().c
|
const totalUsers = instance.prepare('SELECT COUNT(*) as c FROM profiles').get().c
|
||||||
const avgScore = instance.prepare('SELECT AVG(score) as a FROM user_progress').get().a || 0
|
const avgScore = instance.prepare('SELECT AVG(score) as a FROM user_progress').get().a ?? 0
|
||||||
const mostMissed = instance.prepare(`
|
const mostMissed = instance.prepare(`
|
||||||
SELECT letter, SUM(attempts - correct) as misses
|
SELECT letter, SUM(attempts - correct) as misses
|
||||||
FROM letter_stats GROUP BY letter ORDER BY misses DESC LIMIT 5
|
FROM letter_stats GROUP BY letter ORDER BY misses DESC LIMIT 5
|
||||||
|
|||||||
@@ -159,6 +159,17 @@ describe('mnemonics', () => {
|
|||||||
assert.equal(m.A, 'go WALKING')
|
assert.equal(m.A, 'go WALKING')
|
||||||
assert.equal(m.E, 'go')
|
assert.equal(m.E, 'go')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('saveMnemonic awards 50 pts on first save, not on update', () => {
|
||||||
|
const p1 = db.getOrCreateProgress(profileId)
|
||||||
|
db.saveMnemonic(profileId, 'T', 'dash') // first save → +50 pts
|
||||||
|
const p2 = db.getOrCreateProgress(profileId)
|
||||||
|
assert.equal(p2.score, p1.score + 50)
|
||||||
|
|
||||||
|
db.saveMnemonic(profileId, 'T', 'updated dash') // update → no extra pts
|
||||||
|
const p3 = db.getOrCreateProgress(profileId)
|
||||||
|
assert.equal(p3.score, p2.score)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('letter stats', () => {
|
describe('letter stats', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user