fix(functions): server-authoritative, mode-aware, deterministic daily question
Replace pickRandomQuestionId (empty pool → unresolvable q_default_daily fallback) with pickDailyQuestionId(date): computes today's weekday mode (mirrors the client DailyModeResolver.DOW_DEFAULTS, FROZEN) and deterministically indexes the free daily pool by epochDay % poolSize — the SAME question the free client would pick, now assigned server-side. Fixes two things at the root: - The daily 'questions' pool was empty, so every couple got q_default_daily, which the client can't resolve → each device fell back to its own local selection (the DQ-MISMATCH-001 class; the client half was pinned to the free pool in C1). - Partners in different time zones computed different device-local weekdays and got different questions; a single server-assigned id makes both devices identical. Firestore 'questions' seeded with the 75 free daily_fun_mc weekday questions (ids match the client asset DB so getQuestionById resolves them). Pool fetched once and filtered in memory — no composite index needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fda53175a9
commit
f2321d3536
|
|
@ -23,9 +23,9 @@ export const assignDailyQuestion = functions.pubsub
|
|||
const today = cstDateString()
|
||||
const nextDay = nextCstDateString()
|
||||
|
||||
const questionId = await pickRandomQuestionId()
|
||||
const questionId = await pickDailyQuestionId(today)
|
||||
if (!questionId) {
|
||||
console.error('[assignDailyQuestion] no active questions available')
|
||||
console.error('[assignDailyQuestion] questions pool not seeded — skipping assignment')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ export const assignDailyQuestionCallable = functions.https.onCall(async (data: R
|
|||
if (date !== today) {
|
||||
throw new functions.https.HttpsError('invalid-argument', 'Daily question can only be assigned for today.')
|
||||
}
|
||||
const questionId = await pickRandomQuestionId()
|
||||
const questionId = await pickDailyQuestionId(today)
|
||||
if (!questionId) {
|
||||
throw new functions.https.HttpsError('internal', 'No active questions available.')
|
||||
}
|
||||
|
|
@ -150,22 +150,53 @@ export const assignDailyQuestionCallable = functions.https.onCall(async (data: R
|
|||
* so the app still functions during rollout. In production, keep the local
|
||||
* Room database and Firestore pool in sync through the existing seed flow.
|
||||
*/
|
||||
async function pickRandomQuestionId(): Promise<string | null> {
|
||||
// Weekday → daily mode tag. FROZEN to mirror the client's DailyModeResolver.DOW_DEFAULTS
|
||||
// (app/.../domain/DailyModeResolver.kt); JS getUTCDay(): 0=Sun … 6=Sat.
|
||||
const WEEKDAY_MODE_TAGS: Record<number, string> = {
|
||||
0: 'mode_tiny_date_night', // Sunday — Slow Burn Sunday
|
||||
1: 'mode_soft_monday', // Monday — Mood Check Monday
|
||||
2: 'mode_snack_mission', // Tuesday — Tiny Win Tuesday
|
||||
3: 'mode_no_phone_moment', // Wednesday — Real One Wednesday
|
||||
4: 'mode_laugh_reset', // Thursday — Laugh It Off Thursday
|
||||
5: 'mode_flirty_friday', // Friday — Flirty Friday
|
||||
6: 'mode_weekend_side_quest',// Saturday — Side Quest Saturday
|
||||
}
|
||||
|
||||
/** Days since epoch for a YYYY-MM-DD Chicago date — mirrors LocalDate.toEpochDay() the client
|
||||
* uses for its deterministic daily offset, so server and client land on the same question. */
|
||||
function epochDayOf(dateStr: string): number {
|
||||
const [y, m, d] = dateStr.split('-').map(Number)
|
||||
return Math.floor(Date.UTC(y, m - 1, d) / 86_400_000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministically pick the SAME daily question the free client would compute for [dateStr]:
|
||||
* today's weekday mode, ordered by id, indexed by epochDay % poolSize. Making this SERVER-side and
|
||||
* authoritative also fixes partners in different time zones getting different questions (the client
|
||||
* falls back to device-local weekday only when there is no server assignment). Returns null if the
|
||||
* questions pool is unseeded so the caller can no-op rather than write an unresolvable id.
|
||||
*/
|
||||
async function pickDailyQuestionId(dateStr: string): Promise<string | null> {
|
||||
const db = admin.firestore()
|
||||
// The whole free daily pool is tiny (~75 rows); fetch once and filter in memory to avoid a
|
||||
// composite index on (active, isPremium, modeTag).
|
||||
const snapshot = await db
|
||||
.collection('questions')
|
||||
.where('active', '==', true)
|
||||
.where('isPremium', '==', false)
|
||||
.get()
|
||||
if (snapshot.empty) return null
|
||||
|
||||
if (snapshot.empty) {
|
||||
// Rollout fallback: keep the feature working until the questions pool is seeded.
|
||||
return 'q_default_daily'
|
||||
}
|
||||
|
||||
const docs = snapshot.docs
|
||||
const chosen = docs[Math.floor(Math.random() * docs.length)]
|
||||
return chosen.id
|
||||
const weekday = new Date(`${dateStr}T12:00:00Z`).getUTCDay()
|
||||
const modeTag = WEEKDAY_MODE_TAGS[weekday]
|
||||
const inMode = snapshot.docs
|
||||
.filter((d) => d.get('modeTag') === modeTag)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
const pool = inMode.length > 0
|
||||
? inMode
|
||||
: snapshot.docs.slice().sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) // wildcard/empty-mode fallback
|
||||
const offset = ((epochDayOf(dateStr) % pool.length) + pool.length) % pool.length
|
||||
return pool[offset].id
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue