Skip to content

release: v0.2.0#3

Merged
babatonga merged 11 commits into
masterfrom
release/v0.2.0
May 12, 2026
Merged

release: v0.2.0#3
babatonga merged 11 commits into
masterfrom
release/v0.2.0

Conversation

@babatonga

Copy link
Copy Markdown
Owner

Summary

Sammel-PR mit 5 zusammenhängenden Verbesserungen seit v0.1.0:

  • feat(ai): Talent-Namen + Hero-Talents auflösen (feat/talent-name-resolution)
    Importiert TraitNodeEntry + TraitDefinition + TraitSubTree aus wago.tools und materialisiert eine neue kind=talent-Schicht in wow_localizations. Talent-IDs aus combatantInfo.talentTree (TraitNodeEntry-IDs, nicht Spell-IDs) werden jetzt korrekt aufgelöst — vorher kollidierten sie mit alten Spell-IDs und gaben confidently-wrong Namen ("Egg Shell" statt "Festermight"). Hero-Talent-Tree-Picks ebenfalls drin via TraitSubTree.

  • fix(email): Password-Reset funktioniert + Dark-Mode-Kontrast (fix/email-async-render)
    enable_async=True aus Jinja-Env entfernt (synchroner render() aus async-Loop crashte mit asyncio.run-from-loop). Email-Templates kriegen explizite Inline-Farben damit der Body in Dark-Mode-Mail-Clients lesbar bleibt.

  • feat(ai): Wipe-aware Analyse (fix/wipe-aware-analysis)
    Nutzt jetzt die WCL-activeTime des Spielers als Denominator für per-Minute-Raten, statt der vollen Fight-Dauer. Ein Tod bei 1:30 in einem 6:00 Wipe verfälscht die Rotation-Density nicht mehr. Prompt kriegt expliziten Wipe-/Death-Block, der den Fokus auf die Todesursache lenkt statt auf vermeintlich fehlende Casts.

  • feat(admin): Reasoning-Effort-Dropdown im Admin-Panel (fix/admin-openai-reasoning-effort)
    Spiegelt das BYOK-Profil-Panel: Admins können die OpenAI GPT-5/o-Series Reasoning-Tiefe per UI setzen statt nur via OPENAI_REASONING_EFFORT env (vorher: Redeploy nötig).

  • fix(openai): Base-URL-Default wenn env leer (fix/openai-base-url-default)
    OPENAI_BASE_URL='' in env crashte den Admin-Pfad mit UnsupportedProtocol: Request URL is missing 'http://' or 'https://' protocol, weil das OpenAI-SDK den leeren Env-Wert verbatim als base_url verwendet hat. Jetzt fällt der Provider auf die kanonische https://api.openai.com/v1 zurück.

Test plan

  • Jeder Branch lokal end-to-end verifiziert (echter WCL-Report-Import + AI-Aufruf wo zutreffend)
  • Alle 5 Branches in release/v0.2.0 ohne Konflikte gemergt
  • AST-parse aller geänderten Python-Files clean
  • Feature-Koexistenz im Analyzer geprüft (active_time + admin reasoning_effort + talent_ids alle drin)
  • CI grün auf Tag v0.2.0 (Tests + Image-Build)
  • Portainer-Pull auf prod + Smoke-Test

🤖 Generated with Claude Code

babatonga and others added 11 commits May 10, 2026 03:14
…finition

The IDs WCL ships in combatantInfo.talentTree (e.g. 96173) are
TraitNodeEntry IDs, not Spell IDs — they share the same numeric range
as old MoP-era spells but mean something completely different. Looking
them up under kind=spell returned confidently-wrong names like
"Corrupted Egg Shell" or "Faerie Dragon's Song" for what are actually
Death Knight talents, so the AI refused talent recommendations
("Keine seriöse Talent-Empfehlung möglich, weil keine Talent-IDs im
Snapshot stehen").

Fix: import the Trait* DBC chain from wago.tools and materialise a new
``kind=talent`` row per (TraitNodeEntry.ID, locale) where the name
comes from the chain

    TraitNodeEntry.ID
      → TraitDefinition (via TraitDefinitionID)
          → OverrideName_lang  (explicit per-talent name, if any)
          → VisibleSpellID     (what the in-game UI shows)
          → SpellID            (canonical fallback)

The talent phase runs after spells in the orchestrator since it depends
on the spell-name cache. Talents are best-effort: failing wago tables
don't fail the overall import.

Analyzer + lookup_names now route ``talent_ids`` to ``kind=talent``
exclusively — never fall back to ``kind=spell``, since a wrong-but-
confident name is worse than no name. Prompt updated to teach the model
about the new ``talent:<id>`` map keys and explicitly warn against
cross-namespace lookups.

Verified end-to-end: 21144 talent rows imported (EN+DE), DK Unholy IDs
that previously returned "Corrupted Egg Shell" now return
"Runic Attenuation" / "Runischer Kreislauf"; full report import resolves
77/78 Frost Mage talent_ids with 0 spell-namespace collisions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous import only handled class/spec talents (TraitDefinition chain),
which left the hero-tree selection nodes unresolved — and those carry a
huge gameplay weight ("Spellslinger" vs "Frostfire" for a Frost Mage,
"Sanlayn" vs "Deathbringer" for a DK, etc.). Without these, the AI was
still missing the most important single talent decision per spec.

Hero-tree picks live as TraitNodeEntry rows with TraitDefinitionID=0
and a non-zero TraitSubTreeID (NodeEntryType=2). We now download the
TraitSubTree DBC alongside TraitDefinition and emit a kind=talent row
for each selection entry:

    TraitNodeEntry.ID
      → TraitSubTree.Name_lang  (via TraitSubTreeID)

The resulting names are prefixed with "Hero-Talents:" / "Heldentalente:"
so the AI doesn't confuse a tree pick with an individual talent node.

Verified locally: full 460-player report now has 1442/1442 talent_ids
resolved (100%, up from 1417/1442). Per-player E2E test went from 77/78
to 78/78. Total cache size +262 rows (131 selection entries × 2 locales).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents

Two issues hit the password-reset path simultaneously:

1) Jinja env was created with ``enable_async=True`` while ``_render`` calls
   the *sync* ``template.render(...)``. With async mode on, that wraps the
   call in ``asyncio.run(template.render_async(...))`` which crashes from
   inside an already-running event loop:

       RuntimeError: asyncio.run() cannot be called from a running event
       loop

   Result: the password-reset endpoint logged a stacktrace and the user
   never received the mail. Email templates are tiny — sync rendering is
   plenty fast, so the cleanest fix is to drop the async flag.

2) Body text in the HTML templates relied on body-level ``color:#e5e7eb``
   without inline color on the ``<p>`` elements. Most mail clients (Gmail,
   Apple Mail, Outlook web) discard or override body-level styles on
   nested elements, especially under their own dark-mode logic, leaving
   the body text barely legible against the intentionally dark theme.

   Fix: explicit inline color on every text paragraph plus
   ``color-scheme`` / ``supported-color-schemes`` meta tags so clients
   leave our dark theme alone instead of auto-darkening it.

Verified locally by sending real mail to a real inbox via the host's SMTP
relay; greeting renders correctly and body text is now readable in dark
mail clients.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation

When a user requests analysis on a wipe where they died early, the
previous prompt fed the *full fight duration* as the denominator for
``casts_per_minute`` and friends. So a player who died at 1:30 in a
6:00 wipe got blamed for having ~4× too few casts per minute compared
to the top-log kill references — even though their rotation density was
actually fine during the time they were alive.

WCL already ships ``activeTime`` per player in the DamageDone/Healing
tables (the time the player was alive and in combat). The parser was
quietly using it internally to compute DPS/HPS but throwing it away
afterwards. We now surface it through three layers:

  parser  →  per-player ``active_time_ms`` on damage/healing rows
  service →  persisted to ``report_players.extras.active_time_ms`` as
             max(damage_active, healing_active)
  analyzer →  exposed as ``player.active_time_minutes`` in the prompt,
             AND used as the denominator for the player's own
             ``casts_per_minute`` / ``hits_per_minute`` / ``total_per_minute``
             annotations (so the rates are already honest by the time
             the model sees them — no double normalisation)

Top-log references stay normalised against their fight duration —
WCL's leaderboard only ships kills, so duration ≈ active time for
references, and the user's active-time rates compare apples-to-apples
against ref kill-duration rates.

Prompts (EN + DE) get an explicit "Wipe + death awareness" block:
- explains that per-minute fields are pre-normalised against active
  time, so the model must NOT scale them down again,
- tells the model to divide expected-CD-uses by ``active_time_minutes``
  (a 3-min CD on a 1:30 alive player has expected 0-1 uses, not 2),
- forbids blaming the player for rotation casts they "should have
  had time for" relative to the wipe length,
- redirects focus to the actual cause of death: read damage_taken /
  debuffs for the lethal mechanic, flag missing defensive casts as
  critical findings.

Verified locally on a real TWW Manaforge Omega report:
- Wipe-deaths show active_time at 4-46% of fight duration (Imbecile
  4.3%, Aurisdiamond 23.5%, Lorki 46.3%) — the cases where the bug
  was visibly hurting the analysis,
- Kill survivors show active ≈ 99.8-99.9% of fight duration (only the
  out-of-combat pre-pull/post-kill seconds differ), so the fix is a
  no-op for the case that was already working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-user BYOK profile panel (UserAiConfigPanel) had a
``reasoning_effort`` dropdown for GPT-5 / o-series. The app-wide admin
settings panel did not — admins running the shared OPENAI_API_KEY could
only set reasoning depth via the OPENAI_REASONING_EFFORT env, which
required a redeploy to change. This brings the admin lever in line with
the user one.

Backend
- AdminSettingsOut / AdminSettingsUpdate get ``openai_reasoning_effort``.
- /api/v1/admin/settings GET reads it from the ``openai_reasoning_effort``
  AppSetting key (falling back to env), PATCH persists it. Junk values
  and empty strings are coerced to "no override" via a single
  ``_normalize_reasoning_effort`` helper so the GET+PATCH round-trip
  agrees on what counts as cleared.
- analyzer.py: new ``_resolve_openai_reasoning_effort`` helper reads the
  AppSetting; ``_provider_for("openai", ...)`` plumbs the value through
  to ``OpenAiCompatibleProvider`` only when chosen_provider==openai.

Frontend
- AdminSettings type + admin/page.tsx mirror the UserAiConfigPanel
  dropdown (Aus / minimal / low / medium / high), only shown when the
  active provider is OpenAI. State hydrates from the GET, PATCH always
  ships the field so flipping provider to anthropic doesn't leave a
  stale stored override behind.
- i18n keys (DE + EN): admin.aiReasoningEffort / aiReasoningEffortOff /
  aiReasoningEffortHint.

Semantics deliberately mirror the BYOK panel: "Aus" (empty) means "no
override → fall back to OPENAI_REASONING_EFFORT env"; any of
minimal/low/medium/high is an explicit override. The provider itself
was already trinary-correct, so no provider changes were needed.

Verified locally with two smoke tests against the actual backend
endpoints + analyzer helper:
- _resolve_openai_reasoning_effort handles no-row / empty / valid /
  junk inputs correctly (None / None / "high" / None).
- /admin/settings GET → PATCH("medium") → GET returns "medium",
  PATCH("") clears, PATCH("ultra") silently coerces to cleared.

Other BYOK-vs-admin parity check: reasoning_effort was the only real
gap. Remaining differences (BYOK has openai_compatible provider type,
base_url, free-text model, label; admin has dropdown ai_model only)
are intentional — BYOK is per-user with the user's own key, admin is
app-wide with env-managed secrets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When .env ships ``OPENAI_BASE_URL=`` (empty, the default for a clean
install), our provider was passing ``base_url=None`` to the OpenAI SDK
in the admin / app-wide path. The SDK then falls back to
``os.environ["OPENAI_BASE_URL"]`` and uses the empty string verbatim,
so every chat-completion request URL ended up missing its scheme:

    httpx.UnsupportedProtocol: Request URL is missing an 'http://' or
    'https://' protocol.

Bug is reachable only on the app-wide OpenAI path (admin chose
``ai_provider=openai`` and ran an analysis). BYOK profile path already
hardcodes a canonical default in ``user_ai_service.build_provider_for_user``,
which is why per-user analyses kept working.

Fix: hardcode the canonical OpenAI URL as a module-level constant and
use it as a fallback in both the admin path and (defensively) the BYOK
``mode=="openai"`` branch when the caller passes an empty / missing
URL. The ``mode=="local"`` path is unchanged — it has its own env var.

Verified locally with 4 cases:
- Admin path with empty env override        → canonical URL.
- BYOK with explicit "https://my-proxy/..." → that URL.
- BYOK with empty string                    → canonical URL.
- BYOK with None                            → canonical URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default the OpenAI base URL to the canonical https://api.openai.com/v1
when our OPENAI_BASE_URL env is empty, instead of passing None and
letting the SDK fall back to a stray empty env override that produces
URLs without a scheme.
Drop enable_async=True from the Jinja env (synchronous render() inside a
running event loop was crashing with asyncio.run-from-loop). Also pin
explicit text colours on email-template paragraphs so dark-mode mail
clients don't render the body grey-on-grey.
Resolve TraitNodeEntry IDs from combatantInfo.talentTree to real talent
names (incl. hero-tree picks via TraitSubTree) so the AI can actually
cite them, instead of silently looking them up under kind=spell and
hitting confidently-wrong MoP-era spell names.
Use the player's WCL activeTime instead of the full fight duration as
the denominator for per-minute rates, so a player who died early in a
6:00 wipe doesn't look like they had 4x too few casts. Prompt also
gets an explicit wipe + death awareness block that redirects focus to
the actual cause of death.
Expose OpenAI reasoning_effort dropdown in the admin settings panel,
mirroring the per-user BYOK panel. Backend persists the override in the
openai_reasoning_effort AppSetting, analyzer plumbs it through to the
OpenAiCompatibleProvider only when chosen_provider==openai.
@babatonga
babatonga merged commit 3ded015 into master May 12, 2026
3 checks passed
@babatonga
babatonga deleted the release/v0.2.0 branch May 12, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant