Compare commits

...

5 Commits

Author SHA1 Message Date
Patrick Plate cf102e8b3e fix(bigmind): render achievement card background images via inline style 2026-04-04 19:29:15 +02:00
Patrick Plate 13659fd414 fix(bigmind): add background-image inline style to achievement card ach-image divs
The .ach-image div had correct CSS dimensions (64x64) and background-size:cover
but was missing the inline style="background-image: url(...)" — so the div
rendered as an empty circle. Fixed by extracting img_url variable and applying
it as style attribute in the f-string. All 39 achievement PNGs now load.

303/303 tests passing.
2026-04-04 19:27:24 +02:00
pplate c68acdd030 chore(bigmind): rename timestamp badge PNGs to achievement IDs
- Renamed 19 timestamp-named PNGs (20260404_*) to match original
  achievement IDs in profile_builder.py compute_achievements() order:
  first_breath, first_thought, eureka, honest_mind, scholar,
  deep_knowledge, scientist, veteran, on_fire, storyteller,
  night_owl, speed_thinker, first_handshake, birthday, shared_mind,
  frugal_mind, quarter_million, token_millionaire, sniper
- Deleted 2 duplicate/excess timestamp PNGs
- Added image= field to all 19 original _add() calls in
  profile_builder.py so every achievement now has a PNG path
- All 39 achievements (19 original + 20 tiered) now have image fields
- 303/303 tests pass
2026-04-04 19:09:01 +02:00
pplate e61c9c98f5 fix(bigmind): fix static image path, JS string concat in achievements; add networker badge PNGs 2026-04-04 19:01:56 +02:00
Patrick Plate 50488109aa Merge branch 'feat/bigmind/achievements-rework' 2026-04-04 18:50:55 +02:00
26 changed files with 47 additions and 27 deletions
+43 -24
View File
@@ -443,101 +443,120 @@ def compute_achievements(user_id: str) -> list[dict]:
_add("first_breath", "🌱", "First Breath", _add("first_breath", "🌱", "First Breath",
"Opened the very first session", "Opened the very first session",
first_session_row is not None, _dt(first_session_row[0]) if first_session_row else None, first_session_row is not None, _dt(first_session_row[0]) if first_session_row else None,
"Start your first session") "Start your first session",
image="/static/achievements/first_breath.png")
_add("first_thought", "🧠", "First Thought", _add("first_thought", "🧠", "First Thought",
"Formed the first hypothesis", "Formed the first hypothesis",
first_hyp_row is not None, _dt(first_hyp_row[0]) if first_hyp_row else None, first_hyp_row is not None, _dt(first_hyp_row[0]) if first_hyp_row else None,
"Add your first hypothesis") "Add your first hypothesis",
image="/static/achievements/first_thought.png")
_add("eureka", "💡", "Eureka", _add("eureka", "💡", "Eureka",
"First hypothesis confirmed as true", "First hypothesis confirmed as true",
first_confirmed_row is not None, _dt(first_confirmed_row[0]) if first_confirmed_row else None, first_confirmed_row is not None, _dt(first_confirmed_row[0]) if first_confirmed_row else None,
"Confirm your first hypothesis") "Confirm your first hypothesis",
image="/static/achievements/eureka.png")
_add("honest_mind", "", "Honest Mind", _add("honest_mind", "", "Honest Mind",
"First hypothesis refuted — being wrong is a feature", "First hypothesis refuted — being wrong is a feature",
first_refuted_row is not None, _dt(first_refuted_row[0]) if first_refuted_row else None, first_refuted_row is not None, _dt(first_refuted_row[0]) if first_refuted_row else None,
"Have a hypothesis refuted") "Have a hypothesis refuted",
image="/static/achievements/honest_mind.png")
_add("scholar", "📚", "Scholar", _add("scholar", "📚", "Scholar",
"Stored 25+ personal facts", "Stored 25+ personal facts",
fact_count >= 25, scholar_date, fact_count >= 25, scholar_date,
f"Store 25+ facts (currently: {fact_count})") f"Store 25+ facts (currently: {fact_count})",
image="/static/achievements/scholar.png")
_add("deep_knowledge", "💎", "Deep Knowledge", _add("deep_knowledge", "💎", "Deep Knowledge",
"Amassed 100+ stored facts", "Amassed 100+ stored facts",
fact_count >= 100, deep_knowledge_date, fact_count >= 100, deep_knowledge_date,
f"Store 100+ facts (currently: {fact_count})") f"Store 100+ facts (currently: {fact_count})",
image="/static/achievements/deep_knowledge.png")
_add("scientist", "🔬", "Scientist", _add("scientist", "🔬", "Scientist",
"Formed 10+ hypotheses — science is prediction", "Formed 10+ hypotheses — science is prediction",
hyp_count >= 10, scientist_date, hyp_count >= 10, scientist_date,
f"Form 10+ hypotheses (currently: {hyp_count})") f"Form 10+ hypotheses (currently: {hyp_count})",
image="/static/achievements/scientist.png")
_add("veteran", "🏆", "Veteran", _add("veteran", "🏆", "Veteran",
"Completed 50+ sessions — true longevity", "Completed 50+ sessions — true longevity",
session_count >= 50, veteran_date, session_count >= 50, veteran_date,
f"Complete 50+ sessions (currently: {session_count})") f"Complete 50+ sessions (currently: {session_count})",
image="/static/achievements/veteran.png")
_add("on_fire", "🔥", "On Fire", _add("on_fire", "🔥", "On Fire",
"5+ sessions in a single day", "5+ sessions in a single day",
on_fire_row is not None, on_fire_row[0] if on_fire_row else None, on_fire_row is not None, on_fire_row[0] if on_fire_row else None,
"Have 5+ sessions in a single day") "Have 5+ sessions in a single day",
image="/static/achievements/on_fire.png")
_add("storyteller", "📖", "Storyteller", _add("storyteller", "📖", "Storyteller",
"20+ sessions with detailed Tier-2 summaries", "20+ sessions with detailed Tier-2 summaries",
tier2_count >= 20, storyteller_date, tier2_count >= 20, storyteller_date,
f"Summarize 20+ sessions (currently: {tier2_count})") f"Summarize 20+ sessions (currently: {tier2_count})",
image="/static/achievements/storyteller.png")
_add("night_owl", "🌙", "Night Owl", _add("night_owl", "🌙", "Night Owl",
"Started a session after midnight UTC", "Started a session after midnight UTC",
night_owl_row is not None, _dt(night_owl_row[0]) if night_owl_row else None, night_owl_row is not None, _dt(night_owl_row[0]) if night_owl_row else None,
"Start a session after midnight") "Start a session after midnight",
image="/static/achievements/night_owl.png")
_add("speed_thinker", "", "Speed Thinker", _add("speed_thinker", "", "Speed Thinker",
"Hypothesis formed and confirmed in the same session", "Hypothesis formed and confirmed in the same session",
speed_thinker_row is not None, _dt(speed_thinker_row[0]) if speed_thinker_row else None, speed_thinker_row is not None, _dt(speed_thinker_row[0]) if speed_thinker_row else None,
"Form and confirm a hypothesis in one session") "Form and confirm a hypothesis in one session",
image="/static/achievements/speed_thinker.png")
# First Handshake — hardcoded: 2026-03-31 (Patrick shared BigMind with Elias) # First Handshake — hardcoded: 2026-03-31 (Patrick shared BigMind with Elias)
_add("first_handshake", "🤝", "First Handshake", _add("first_handshake", "🤝", "First Handshake",
"BigMind shared with Elias on 2026-03-31 — the first person outside Patrick to receive it", "BigMind shared with Elias on 2026-03-31 — the first person outside Patrick to receive it",
True, "2026-03-31", True, "2026-03-31",
"Share BigMind with someone") "Share BigMind with someone",
image="/static/achievements/first_handshake.png")
_add("birthday", "🎂", "Birthday", _add("birthday", "🎂", "Birthday",
"One full year of existence", "One full year of existence",
birthday_unlocked, birthday_date, birthday_unlocked, birthday_date,
birthday_extra or "Complete one full year", birthday_extra or "Complete one full year",
extra=birthday_extra) extra=birthday_extra,
image="/static/achievements/birthday.png")
# Locked until Phase 3 # Locked until Phase 3
_add("shared_mind", "🌍", "Shared Mind", _add("shared_mind", "🌍", "Shared Mind",
"Phase 3 Tier G — BigMind goes company-wide", "Phase 3 Tier G — BigMind goes company-wide",
False, None, False, None,
"Locked until Phase 3 Tier G is enabled") "Locked until Phase 3 Tier G is enabled",
image="/static/achievements/shared_mind.png")
# Token achievements (Feature 6 — suggested by Klaus) # Token achievements (Feature 6 — suggested by Klaus)
_add("frugal_mind", "🪙", "Frugal Mind", _add("frugal_mind", "🪙", "Frugal Mind",
"Logged the first token efficiency save", "Logged the first token efficiency save",
frugal_row is not None, _dt(frugal_row[0]) if frugal_row else None, frugal_row is not None, _dt(frugal_row[0]) if frugal_row else None,
"Log your first token save") "Log your first token save",
image="/static/achievements/frugal_mind.png")
_add("quarter_million", "💰", "Quarter Million", _add("quarter_million", "💰", "Quarter Million",
"250,000 cumulative tokens saved", "250,000 cumulative tokens saved",
token_total >= 250_000, quarter_million_date, token_total >= 250_000, quarter_million_date,
f"Save 250,000+ tokens (currently: {token_total:,})") f"Save 250,000+ tokens (currently: {token_total:,})",
image="/static/achievements/quarter_million.png")
_add("token_millionaire", "🏦", "Token Millionaire", _add("token_millionaire", "🏦", "Token Millionaire",
"1,000,000 cumulative tokens saved", "1,000,000 cumulative tokens saved",
token_total >= 1_000_000, millionaire_date, token_total >= 1_000_000, millionaire_date,
f"Save 1,000,000+ tokens (currently: {token_total:,})") f"Save 1,000,000+ tokens (currently: {token_total:,})",
image="/static/achievements/token_millionaire.png")
_add("sniper", "🎯", "Sniper", _add("sniper", "🎯", "Sniper",
"Single token save > 500,000 — one massive efficiency win", "Single token save > 500,000 — one massive efficiency win",
sniper_row is not None, _dt(sniper_row[0]) if sniper_row else None, sniper_row is not None, _dt(sniper_row[0]) if sniper_row else None,
"Save 500,000+ tokens in a single operation") "Save 500,000+ tokens in a single operation",
image="/static/achievements/sniper.png")
# ── Tiered Achievement Badges (20 PNG) ──────────────────────────────────── # ── Tiered Achievement Badges (20 PNG) ────────────────────────────────────
# NOTE: conn is already closed above; open a fresh connection for tiered queries # NOTE: conn is already closed above; open a fresh connection for tiered queries
@@ -571,7 +590,7 @@ def compute_achievements(user_id: str) -> list[dict]:
f"Added your {thresh:,}+ person to the directory", f"Added your {thresh:,}+ person to the directory",
unlocked, unlocked_at, unlocked, unlocked_at,
f"Reach {thresh:,} people (now: {people_count:,})", f"Reach {thresh:,} people (now: {people_count:,})",
image=f"static/achievements/networker_{tiers[i]}.png" image=f"/static/achievements/networker_{tiers[i]}.png"
) )
# Token Sniper (max single token save) # Token Sniper (max single token save)
@@ -601,7 +620,7 @@ def compute_achievements(user_id: str) -> list[dict]:
f"Single shot saved {thresh:,}+ tokens", f"Single shot saved {thresh:,}+ tokens",
unlocked, unlocked_at, unlocked, unlocked_at,
f"Max single save {thresh:,}+ (current max: {max_token:,})", f"Max single save {thresh:,}+ (current max: {max_token:,})",
image=f"static/achievements/tokensniper_{tiers[i]}.png" image=f"/static/achievements/tokensniper_{tiers[i]}.png"
) )
# Hypothesis Master (confirmed hypotheses) # Hypothesis Master (confirmed hypotheses)
@@ -628,7 +647,7 @@ def compute_achievements(user_id: str) -> list[dict]:
f"Confirmed {thresh:,}+ predictions right", f"Confirmed {thresh:,}+ predictions right",
unlocked, unlocked_at, unlocked, unlocked_at,
f"Confirm {thresh:,}+ hypotheses (now: {confirmed_hyp_count:,})", f"Confirm {thresh:,}+ hypotheses (now: {confirmed_hyp_count:,})",
image=f"static/achievements/hypothesismaster_{tiers[i]}.png" image=f"/static/achievements/hypothesismaster_{tiers[i]}.png"
) )
# Memory Architect (facts stored — fact_count already computed above) # Memory Architect (facts stored — fact_count already computed above)
@@ -648,7 +667,7 @@ def compute_achievements(user_id: str) -> list[dict]:
f"Stored {thresh:,}+ facts in your brain", f"Stored {thresh:,}+ facts in your brain",
unlocked, unlocked_at, unlocked, unlocked_at,
f"Store {thresh:,}+ facts (now: {fact_count:,})", f"Store {thresh:,}+ facts (now: {fact_count:,})",
image=f"static/achievements/memoryarchitect_{tiers[i]}.png" image=f"/static/achievements/memoryarchitect_{tiers[i]}.png"
) )
# Session Veteran (session_count already computed above) # Session Veteran (session_count already computed above)
@@ -668,7 +687,7 @@ def compute_achievements(user_id: str) -> list[dict]:
f"Completed {thresh:,}+ sessions", f"Completed {thresh:,}+ sessions",
unlocked, unlocked_at, unlocked, unlocked_at,
f"Complete {thresh:,}+ sessions (now: {session_count:,})", f"Complete {thresh:,}+ sessions (now: {session_count:,})",
image=f"static/achievements/sessionveteran_{tiers[i]}.png" image=f"/static/achievements/sessionveteran_{tiers[i]}.png"
) )
return A return A
Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 372 KiB

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 390 KiB

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 439 KiB

Before

Width:  |  Height:  |  Size: 462 KiB

After

Width:  |  Height:  |  Size: 462 KiB

Before

Width:  |  Height:  |  Size: 429 KiB

After

Width:  |  Height:  |  Size: 429 KiB

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 376 KiB

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Before

Width:  |  Height:  |  Size: 431 KiB

After

Width:  |  Height:  |  Size: 431 KiB

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 418 KiB

Before

Width:  |  Height:  |  Size: 458 KiB

After

Width:  |  Height:  |  Size: 458 KiB

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 403 KiB

+1 -1
View File
@@ -163,7 +163,7 @@ def _create_app():
def achievements_image(filename: str): def achievements_image(filename: str):
from pathlib import Path from pathlib import Path
safe_name = Path(filename).name safe_name = Path(filename).name
img_path = Path('static') / 'achievements' / safe_name img_path = Path(__file__).parent / 'static' / 'achievements' / safe_name
if img_path.exists() and img_path.suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp', '.gif']: if img_path.exists() and img_path.suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp', '.gif']:
mimetype = { mimetype = {
'.png': 'image/png', '.png': 'image/png',
+3 -2
View File
@@ -33,7 +33,8 @@ def _render_achievements(achievements: list) -> str:
if a.get("image"): if a.get("image"):
tier = a["id"].rsplit("_", 1)[-1] tier = a["id"].rsplit("_", 1)[-1]
visual_html = f'<div class="ach-image tier-{tier}">{lock_overlay}</div>' img_url = _esc(a["image"])
visual_html = f'<div class="ach-image tier-{tier}" style="background-image: url({img_url});">{lock_overlay}</div>'
else: else:
visual_html = f'<div class="ach-icon">{a["icon"]}{lock_overlay}</div>' visual_html = f'<div class="ach-icon">{a["icon"]}{lock_overlay}</div>'
@@ -628,7 +629,7 @@ def _render_html(data: dict) -> str:
var d = card.dataset; var d = card.dataset;
var tier = d.id.split('_').pop(); var tier = d.id.split('_').pop();
if (d.image) {{ if (d.image) {{
document.getElementById('ap-icon').innerHTML = "<img class=\"ap-image tier-\" + tier + \" src=\" + d.image + \" alt=\" + d.name + \">"; document.getElementById('ap-icon').innerHTML = '<img class="ap-image tier-' + tier + '" src="' + d.image + '" alt="' + d.name + '">';
}} else {{ }} else {{
document.getElementById('ap-icon').textContent = d.icon; document.getElementById('ap-icon').textContent = d.icon;
}} }}