fix(content): give packs a pack_id, and stop icon_name leaking into the UI
Populates question.pack_id (build_db.py). The column was plumbed end to end (question.pack_id -> QuestionEntity.packId -> Question.packId) but was NULL for all 3811 rows, so content had no pack identity. Bundled packs now carry one — the logical id a pack declares in metadata (daily ships as category daily_fun_mc but is logically daily_single_choice_weekly_v1), else its category id. This is the hook a purchased/imported pack needs: it becomes just another pack_id in the same table rather than a special case. No schema or behaviour change; identity hash unchanged. Fixes two regressions the rebuilt db exposed, both latent for the same reason: every category previously carried the placeholder icon_name "question", which masked them. Packs now declare real Material icon names (shield, forum, paid). 1. Category glyphs. categoryGlyphStyle keyed on `iconName ?: categoryId`, so icon_name won. Material names are a different vocabulary from this file's keys, so 22 of 23 categories fell through to the default star. The curated per-category glyph now wins, with icon_name as the fallback — which is also what gives an imported pack a real glyph, since its category_id will not be listed here but its declared icon_name resolves. Added the Material aliases and the two unmapped categories (quality_time, daily_fun_mc). 2. Pack library chips. metadataLabels() rendered the raw icon_name as a user-facing tag, producing chips like "Chat Bubble Outline". It was only ever invisible because the list filtered the single value "Question". icon_name is a rendering detail, not a topic, so the chip is gone; access remains. Verified live on both fixtures (dark 5554 / light 5556): distinct correct glyphs (Boundaries warning, Communication chat, Rebuilding Trust shield, Sex & Desire heart-outline), no leaked chips, and the new Quality Time pack browsable with 150 questions and a calendar glyph. Unit tests green, 0 FATAL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8e6dabd4a4
commit
1ac724bcde
Binary file not shown.
|
|
@ -124,14 +124,31 @@ fun categoryGlyphStyle(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val key = (iconName ?: categoryId).lowercase().trim()
|
// The curated per-category glyph wins; a pack's declared icon_name is the fallback.
|
||||||
return when (key) {
|
// Packs ship Material icon names ("shield", "paid", "forum") — a different vocabulary from the
|
||||||
"communication", "chat", "question", "questions" -> GlyphStyle(
|
// keys below — so letting icon_name take precedence sent almost every category to the default
|
||||||
|
// star. Keeping icon_name as a fallback is what gives an imported/purchased pack a real glyph:
|
||||||
|
// its category_id won't be listed here, so its declared icon_name resolves instead.
|
||||||
|
return glyphStyleForKey(categoryId.lowercase().trim())
|
||||||
|
?: iconName?.let { glyphStyleForKey(it.lowercase().trim()) }
|
||||||
|
?: GlyphStyle(
|
||||||
|
icon = ImageVector.vectorResource(R.drawable.glyph_star),
|
||||||
|
tint = CloserPalette.PurpleDeep,
|
||||||
|
container = CloserPalette.PurpleMist
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolves one key (a category id, or a pack's icon_name) to a glyph, or null if unknown. */
|
||||||
|
@Composable
|
||||||
|
private fun glyphStyleForKey(key: String): GlyphStyle? =
|
||||||
|
when (key) {
|
||||||
|
"communication", "chat", "question", "questions",
|
||||||
|
"chat_bubble_outline", "forum" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_chat),
|
icon = ImageVector.vectorResource(R.drawable.glyph_chat),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleMist
|
container = CloserPalette.PurpleMist
|
||||||
)
|
)
|
||||||
"emotional_intimacy", "intimacy", "heart" -> GlyphStyle(
|
"emotional_intimacy", "intimacy", "heart", "favorite", "volunteer_activism" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_heart),
|
icon = ImageVector.vectorResource(R.drawable.glyph_heart),
|
||||||
tint = CloserPalette.PinkAccentDeep,
|
tint = CloserPalette.PinkAccentDeep,
|
||||||
container = CloserPalette.PinkMist
|
container = CloserPalette.PinkMist
|
||||||
|
|
@ -141,22 +158,22 @@ fun categoryGlyphStyle(
|
||||||
tint = CloserPalette.Romantic,
|
tint = CloserPalette.Romantic,
|
||||||
container = CloserPalette.Romantic.copy(alpha = 0.12f)
|
container = CloserPalette.Romantic.copy(alpha = 0.12f)
|
||||||
)
|
)
|
||||||
"trust", "rebuilding_trust", "privacy" -> GlyphStyle(
|
"trust", "rebuilding_trust", "privacy", "shield", "verified_user" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_shield),
|
icon = ImageVector.vectorResource(R.drawable.glyph_shield),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleGlow
|
container = CloserPalette.PurpleGlow
|
||||||
)
|
)
|
||||||
"conflict", "conflict_repair", "repair" -> GlyphStyle(
|
"conflict", "conflict_repair", "repair", "healing" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_sync),
|
icon = ImageVector.vectorResource(R.drawable.glyph_sync),
|
||||||
tint = CloserPalette.PurpleRich,
|
tint = CloserPalette.PurpleRich,
|
||||||
container = CloserPalette.PurpleMist
|
container = CloserPalette.PurpleMist
|
||||||
)
|
)
|
||||||
"future", "timeline" -> GlyphStyle(
|
"future", "timeline", "explore" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_timeline),
|
icon = ImageVector.vectorResource(R.drawable.glyph_timeline),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleSoft
|
container = CloserPalette.PurpleSoft
|
||||||
)
|
)
|
||||||
"money", "payments" -> GlyphStyle(
|
"money", "payments", "paid" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_attach_money),
|
icon = ImageVector.vectorResource(R.drawable.glyph_attach_money),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleMist
|
container = CloserPalette.PurpleMist
|
||||||
|
|
@ -166,22 +183,23 @@ fun categoryGlyphStyle(
|
||||||
tint = CloserPalette.PurpleRich,
|
tint = CloserPalette.PurpleRich,
|
||||||
container = CloserPalette.PurpleGlow
|
container = CloserPalette.PurpleGlow
|
||||||
)
|
)
|
||||||
"gratitude", "star" -> GlyphStyle(
|
"gratitude", "star", "celebration" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_star),
|
icon = ImageVector.vectorResource(R.drawable.glyph_star),
|
||||||
tint = CloserPalette.PinkAccentDeep,
|
tint = CloserPalette.PinkAccentDeep,
|
||||||
container = CloserPalette.PinkMist
|
container = CloserPalette.PinkMist
|
||||||
)
|
)
|
||||||
"parenting", "family", "people" -> GlyphStyle(
|
"parenting", "family", "people", "family_restroom" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_couple),
|
icon = ImageVector.vectorResource(R.drawable.glyph_couple),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleSoft
|
container = CloserPalette.PurpleSoft
|
||||||
)
|
)
|
||||||
"date_night", "date", "calendar" -> GlyphStyle(
|
"date_night", "date", "calendar", "calendar_heart",
|
||||||
|
"quality_time", "schedule_heart" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_calendar),
|
icon = ImageVector.vectorResource(R.drawable.glyph_calendar),
|
||||||
tint = CloserPalette.PinkAccentDeep,
|
tint = CloserPalette.PinkAccentDeep,
|
||||||
container = CloserPalette.PinkMist
|
container = CloserPalette.PinkMist
|
||||||
)
|
)
|
||||||
"fun", "play" -> GlyphStyle(
|
"fun", "play", "emoji_emotions" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_play),
|
icon = ImageVector.vectorResource(R.drawable.glyph_play),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleGlow
|
container = CloserPalette.PurpleGlow
|
||||||
|
|
@ -191,7 +209,7 @@ fun categoryGlyphStyle(
|
||||||
tint = CloserPalette.Danger,
|
tint = CloserPalette.Danger,
|
||||||
container = CloserPalette.Danger.copy(alpha = 0.10f)
|
container = CloserPalette.Danger.copy(alpha = 0.10f)
|
||||||
)
|
)
|
||||||
"values", "marriage" -> GlyphStyle(
|
"values", "marriage", "favorite_border" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_question_answer),
|
icon = ImageVector.vectorResource(R.drawable.glyph_question_answer),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleMist
|
container = CloserPalette.PurpleMist
|
||||||
|
|
@ -206,15 +224,10 @@ fun categoryGlyphStyle(
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleGlow
|
container = CloserPalette.PurpleGlow
|
||||||
)
|
)
|
||||||
"predict", "psychology" -> GlyphStyle(
|
"predict", "psychology", "self_improvement" -> GlyphStyle(
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_psychology),
|
icon = ImageVector.vectorResource(R.drawable.glyph_psychology),
|
||||||
tint = CloserPalette.PurpleDeep,
|
tint = CloserPalette.PurpleDeep,
|
||||||
container = CloserPalette.PurpleGlow
|
container = CloserPalette.PurpleGlow
|
||||||
)
|
)
|
||||||
else -> GlyphStyle(
|
else -> null
|
||||||
icon = ImageVector.vectorResource(R.drawable.glyph_star),
|
|
||||||
tint = CloserPalette.PurpleDeep,
|
|
||||||
container = CloserPalette.PurpleMist
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,10 +367,11 @@ private fun QuestionPackItem.metadataLabels(): List<String> {
|
||||||
"free" -> "Free"
|
"free" -> "Free"
|
||||||
else -> category.access.displayCategoryName()
|
else -> category.access.displayCategoryName()
|
||||||
}
|
}
|
||||||
return listOf(
|
// Only access is shown. `icon_name` used to be listed here too, but it is a rendering detail,
|
||||||
access,
|
// not a topic: packs declare Material icon names, so it surfaced chips like "Chat Bubble
|
||||||
category.iconName.ifBlank { "question" }.displayCategoryName()
|
// Outline". It went unnoticed because every pack once carried the placeholder icon "question",
|
||||||
).filterNot { it == "Question" }.distinct()
|
// which the list filtered out — so this chip has never actually been visible.
|
||||||
|
return listOf(access).distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,21 @@ def build_answer_config(question: Dict[str, Any], where: str) -> Dict[str, Any]:
|
||||||
# Validation — every problem is fatal
|
# Validation — every problem is fatal
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def pack_id_for(category: Dict[str, Any]) -> str:
|
||||||
|
"""The pack a question belongs to.
|
||||||
|
|
||||||
|
`question.pack_id` is plumbed all the way to the domain model and is what lets content be
|
||||||
|
handled per-pack (query it, gate it, remove it) rather than only per-category. Bundled packs
|
||||||
|
get one too, so a later purchased/imported pack is not a special case — it is just another
|
||||||
|
pack_id in the same table.
|
||||||
|
|
||||||
|
Prefer the logical pack id a pack declares in its metadata (the daily pack ships as category
|
||||||
|
`daily_fun_mc` but is logically `daily_single_choice_weekly_v1`); otherwise the category id.
|
||||||
|
"""
|
||||||
|
meta = category.get("metadata") or {}
|
||||||
|
return meta.get("pack_id") or category["id"]
|
||||||
|
|
||||||
|
|
||||||
def load_packs() -> List[Dict[str, Any]]:
|
def load_packs() -> List[Dict[str, Any]]:
|
||||||
"""Load and hard-validate every pack. Raises on the first structural problem."""
|
"""Load and hard-validate every pack. Raises on the first structural problem."""
|
||||||
files = sorted(JSON_DIR.glob("*.json"))
|
files = sorted(JSON_DIR.glob("*.json"))
|
||||||
|
|
@ -255,6 +270,7 @@ def build(packs: List[Dict[str, Any]], db_schema: Dict[str, Any], out: Path) ->
|
||||||
total = 0
|
total = 0
|
||||||
for pack in packs:
|
for pack in packs:
|
||||||
c = pack["category"]
|
c = pack["category"]
|
||||||
|
pack_id = pack_id_for(c)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT OR REPLACE INTO question_category "
|
"INSERT OR REPLACE INTO question_category "
|
||||||
"(id, display_name, description, access, icon_name) VALUES (?, ?, ?, ?, ?)",
|
"(id, display_name, description, access, icon_name) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
|
@ -276,7 +292,7 @@ def build(packs: List[Dict[str, Any]], db_schema: Dict[str, Any], out: Path) ->
|
||||||
q["type"],
|
q["type"],
|
||||||
json.dumps(q.get("tags", []), separators=(",", ":")),
|
json.dumps(q.get("tags", []), separators=(",", ":")),
|
||||||
json.dumps(build_answer_config(q, where), separators=(",", ":")),
|
json.dumps(build_answer_config(q, where), separators=(",", ":")),
|
||||||
None,
|
pack_id,
|
||||||
pack["mtime"],
|
pack["mtime"],
|
||||||
"active",
|
"active",
|
||||||
q.get("sex"),
|
q.get("sex"),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue