release: v0.2.0#3
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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+TraitSubTreeaus wago.tools und materialisiert eine neuekind=talent-Schicht inwow_localizations. Talent-IDs auscombatantInfo.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=Trueaus Jinja-Env entfernt (synchronerrender()aus async-Loop crashte mitasyncio.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-
activeTimedes 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_EFFORTenv (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 mitUnsupportedProtocol: 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 kanonischehttps://api.openai.com/v1zurück.Test plan
release/v0.2.0ohne Konflikte gemergtv0.2.0(Tests + Image-Build)🤖 Generated with Claude Code