Conversation
Add Patch notes
Add Patchnotes + Owned weapons team variations
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (24)
📝 WalkthroughWalkthroughThis PR adds calculator and tool routes, patch-note publishing, investment metadata, beta-data handling, owned simulation variants, reusable UI components, and route-aware navigation. Legacy routes redirect to ChangesApplication updates
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant LayoutServer
participant PatchNoteCatalog
participant Layout
participant PatchNotesPopup
LayoutServer->>PatchNoteCatalog: Load sorted patch notes
PatchNoteCatalog-->>LayoutServer: Return latest note metadata
LayoutServer-->>Layout: Provide latestPatchNote
Layout->>PatchNotesPopup: Pass latest note
PatchNotesPopup->>PatchNotesPopup: Compare acknowledged slug
PatchNotesPopup-->>Layout: Display or suppress popup
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/character-builds.test.ts`:
- Around line 639-648: Extend the “reaction helpers” test in the
formatReactionName/formatReactionFingerprint suite with an assertion for an
unknown reaction key, verifying formatReactionName uses its title-case fallback
when the key is absent from REACTION_LABELS.
In `@src/lib/character-builds.ts`:
- Around line 130-143: The weapon comparator in the sorting function should not
coerce missing strength values to 0. Compare strength only when both weapons
provide it; otherwise continue to the teams comparison, preserving the existing
ordering for stamped rows and the current non-negative strength behavior.
In `@src/lib/ui/components/HoverTooltip.svelte`:
- Around line 211-212: Update the hover/focus handlers around onEnter and
onLeave in HoverTooltip so pointer activity and focus activity are tracked
independently. Handle pointerenter/pointerleave and focusin/focusout separately,
and call hideTip only when both states are inactive, preserving aria-describedby
and the open tooltip while either interaction remains active.
In `@src/lib/upgrade-priority.test.ts`:
- Around line 67-90: Add an assertion in the “prefers stamped CDN tiers over the
ladder” test for a stamped tier with no impact_tiers/scale labels, such as
resolveUpgradeImpact("solid", 30, ladder). Verify it returns the stamped tier
with its corresponding MERGED_IMPACT_LABELS default label, covering the second
label-resolution path while preserving the existing assertions.
In `@src/lib/upgrade-priority.ts`:
- Around line 21-26: Replace the duplicated `UpgradeTier` union in
`src/lib/upgrade-priority.ts` with a type alias to the imported
`ImportanceImpactTier`, preserving the existing exported `UpgradeTier` name and
updating no other behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b851be28-9945-423d-b5f5-780c74add72a
⛔ Files ignored due to path filters (5)
.dockerignoreis excluded by none and included by none.github/workflows/patch-notes.ymlis excluded by none and included by nonepackage.jsonis excluded by none and included by nonepatch-notes/2026-08-10-roster-hotfix-and-patch-notes.mdis excluded by none and included by nonepatch-notes/README.mdis excluded by none and included by none
📒 Files selected for processing (35)
src/lib/character-builds.test.tssrc/lib/character-builds.tssrc/lib/investment-build-card.tssrc/lib/investment-teams.test.tssrc/lib/investment-teams.tssrc/lib/patch-notes-catalog.tssrc/lib/patch-notes-seen.test.tssrc/lib/patch-notes-seen.tssrc/lib/patch-notes.test.tssrc/lib/patch-notes.tssrc/lib/site.tssrc/lib/types/investment.tssrc/lib/ui/NavBar.sveltesrc/lib/ui/components/CostPopover.sveltesrc/lib/ui/components/HoverTooltip.sveltesrc/lib/ui/components/InvestmentBuildCard.sveltesrc/lib/ui/components/PatchNotesPopup.sveltesrc/lib/upgrade-priority.test.tssrc/lib/upgrade-priority.tssrc/routes/+layout.server.tssrc/routes/+layout.sveltesrc/routes/+layout.tssrc/routes/abyss/+page.sveltesrc/routes/characters/+page.sveltesrc/routes/characters/[slug]/+page.sveltesrc/routes/dev/ui/+page.sveltesrc/routes/patch-notes/+page.server.tssrc/routes/patch-notes/+page.sveltesrc/routes/patch-notes/[slug]/+page.server.tssrc/routes/patch-notes/[slug]/+page.sveltesrc/routes/settings/panels/RosterPanel.sveltesrc/routes/stygian/+page.sveltesrc/routes/teams/[slug]/+page.sveltesrc/routes/teams/configs/[slug]/+page.server.tssrc/routes/teams/configs/[slug]/+page.svelte
| return [...weapons].sort((a, b) => { | ||
| const ra = getStars(a.key); | ||
| const rb = getStars(b.key); | ||
| if (ra !== rb) return rb - ra; | ||
| const sa = a.strength ?? 0; | ||
| const sb = b.strength ?? 0; | ||
| if (sa !== sb) return sb - sa; | ||
| if (a.teams !== b.teams) return b.teams - a.teams; | ||
| const pa = preferred.has(a.key) ? 0 : 1; | ||
| const pb = preferred.has(b.key) ? 0 : 1; | ||
| if (pa !== pb) return pa - pb; | ||
| return a.key.localeCompare(b.key); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Locate the producer of CharacterWeaponRank.strength and check its sign/coverage.
set -uo pipefail
# Find any pipeline/merge code that writes a Bradley-Terry strength field.
rg -n -C 6 --iglob '!**/node_modules/**' -e 'bradley' -e '\bstrength\b' -g '*.py' -g '*.ts' -g '*.json'
# Find committed fixtures or sample payloads containing weapon ranks.
fd -t f -e json | xargs -r rg -l '"weapons"' | while IFS= read -r f; do
echo "== $f"
rg -n -C 3 '"strength"' "$f" | head -40
doneRepository: woopxwoop/lightkeepers
Length of output: 377
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '== relevant files =='
git ls-files | rg '(^|/)(character-builds\.ts|investment\.ts)$|strength|weapon' | head -200
printf '%s\n' '== ranking implementation =='
sed -n '100,155p' src/lib/character-builds.ts
printf '%s\n' '== CharacterWeaponRank type documentation =='
sed -n '295,335p' src/lib/types/investment.ts
printf '%s\n' '== strength references and producers =='
rg -n -C 8 --glob '!**/node_modules/**' --glob '!**/dist/**' -e 'bradley' -e '\bstrength\b' .Repository: woopxwoop/lightkeepers
Length of output: 14764
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '== tracked strength references =='
git grep -n -i -E 'strength|CharacterWeaponRank|weapon.*rank|rank.*weapon' -- ':!src/lib/data/weapons.json' | head -300
printf '%s\n' '== investment payload-loading code =='
sed -n '1,240p' src/lib/app/investment.ts
printf '%s\n' '== related tests =='
sed -n '1,180p' src/lib/character-builds.test.ts
printf '%s\n' '== tracked JSON files =='
git ls-files '*.json' | head -200
printf '%s\n' '== JSON strength coverage probe =='
python3 - <<'PY'
import json
import subprocess
from pathlib import Path
files = subprocess.check_output(["git", "ls-files", "*.json"], text=True).splitlines()
found = 0
for name in files:
path = Path(name)
try:
data = json.loads(path.read_text())
except Exception:
continue
def walk(value, loc="$"):
global found
if isinstance(value, dict):
if "strength" in value:
found += 1
print(f"{name}:{loc}: strength={value['strength']!r}, keys={sorted(value)}")
for k, v in value.items():
walk(v, f"{loc}.{k}")
elif isinstance(value, list):
for i, v in enumerate(value):
walk(v, f"{loc}[{i}]")
walk(data)
print(f"objects_with_strength={found}")
PYRepository: woopxwoop/lightkeepers
Length of output: 10403
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '== investment endpoint and data-source files =='
git ls-files | rg -i '(^|/)(api|scripts?|data|static|fixtures?|tests?)(/|$)|investment|character'
printf '%s\n' '== investment endpoint references =='
rg -n -C 10 --glob '!**/node_modules/**' -E '"/api/investment"|/api/investment|InvestmentFile|loadInvestment|fetchInvestment' .
printf '%s\n' '== repository metadata =='
git remote -v
git branch --show-current
git status --short
printf '%s\n' '== package scripts =='
cat package.jsonRepository: woopxwoop/lightkeepers
Length of output: 4851
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '== investment endpoint =='
sed -n '1,240p' 'src/routes/api/investment/+server.ts'
printf '%s\n' '== HTTP client =='
sed -n '1,260p' src/lib/api/http.ts
printf '%s\n' '== investment references =='
rg -n -C 10 --glob '!**/node_modules/**' '/api/investment|InvestmentFile|loadInvestment|fetchInvestment' .
printf '%s\n' '== server environment and URL configuration =='
rg -n -C 8 --glob '!**/node_modules/**' 'investment|INVESTMENT|SUPABASE|DATABASE|API_URL|fetch\(' src svelte.config.* vite.config.* .env* 2>/dev/nullRepository: woopxwoop/lightkeepers
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '== character page data source =='
sed -n '1,220p' 'src/routes/characters/[slug]/+page.server.ts'
printf '%s\n' '== static API endpoint =='
sed -n '1,260p' 'src/routes/api/static/+server.ts'
printf '%s\n' '== CharacterIndex and build types =='
sed -n '1,180p' src/lib/types/investment.ts
printf '%s\n' '== build/static URL helpers =='
rg -n -C 8 --glob '!**/node_modules/**' 'builds|CharacterIndex|api/static|static.*json|cdn.*character|character.*cdn' src/lib src/routes | head -300Repository: woopxwoop/lightkeepers
Length of output: 33926
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '== character summary helper =='
sed -n '1,280p' src/lib/server/character-summary.ts
printf '%s\n' '== URL and CDN configuration =='
rg -n -C 10 --glob '!**/node_modules/**' 'CHARACT|character.*json|summary|cdn|lightkeepers\.moe' src/lib/server src/lib/asset-urls.ts src/lib/utils.ts | head -400
printf '%s\n' '== CDN URL literals =='
git grep -n -E 'https?://[^"]*(character|summary|build|data)[^"]*' -- '*.ts' '*.svelte' '*.json' | head -200Repository: woopxwoop/lightkeepers
Length of output: 23058
🏁 Script executed:
#!/bin/bash
set -u
python3 - <<'PY'
import gzip
import json
import urllib.request
url = "https://images.lightkeepers.moe/sim/characters.json.gz"
req = urllib.request.Request(url, headers={"User-Agent": "read-only-review-probe"})
try:
with urllib.request.urlopen(req, timeout=20) as response:
raw = response.read()
status = response.status
except Exception as exc:
print(f"fetch_error={exc!r}")
raise SystemExit(0)
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
try:
data = json.loads(raw)
except Exception as exc:
print(f"json_error={exc!r}, bytes={len(raw)}")
raise SystemExit(0)
print(f"http_status={status}")
print(f"top_level_type={type(data).__name__}")
if isinstance(data, dict):
print(f"top_level_keys={sorted(data)[:20]}")
characters = data.get("characters", data) if isinstance(data, dict) else {}
if not isinstance(characters, dict):
print(f"character_collection_type={type(characters).__name__}")
raise SystemExit(0)
payloads = rows = present = missing = negative = 0
mixed_payloads = none_strength_payloads = all_strength_payloads = 0
min_strength = None
max_strength = None
missing_examples = []
negative_examples = []
for character_key, payload in characters.items():
weapons = payload.get("weapons") if isinstance(payload, dict) else None
if not isinstance(weapons, list) or not weapons:
continue
payloads += 1
local_present = 0
local_missing = 0
for index, weapon in enumerate(weapons):
if not isinstance(weapon, dict):
continue
rows += 1
if "strength" in weapon and weapon["strength"] is not None:
local_present += 1
present += 1
value = weapon["strength"]
if isinstance(value, (int, float)) and not isinstance(value, bool):
min_strength = value if min_strength is None else min(min_strength, value)
max_strength = value if max_strength is None else max(max_strength, value)
if value < 0:
negative += 1
if len(negative_examples) < 8:
negative_examples.append((character_key, index, weapon.get("key"), value))
else:
local_missing += 1
missing += 1
if len(missing_examples) < 8:
missing_examples.append((character_key, index, weapon.get("key")))
if local_present and local_missing:
mixed_payloads += 1
elif local_present:
all_strength_payloads += 1
elif local_missing:
none_strength_payloads += 1
print(f"characters={len(characters)}")
print(f"payloads_with_weapons={payloads}")
print(f"weapon_rows={rows}")
print(f"strength_present={present}")
print(f"strength_missing_or_null={missing}")
print(f"negative_strength={negative}")
print(f"strength_min={min_strength}")
print(f"strength_max={max_strength}")
print(f"payloads_all_strength={all_strength_payloads}")
print(f"payloads_mixed_strength={mixed_payloads}")
print(f"payloads_no_strength={none_strength_payloads}")
print(f"missing_examples={missing_examples}")
print(f"negative_examples={negative_examples}")
PYRepository: woopxwoop/lightkeepers
Length of output: 332
🏁 Script executed:
#!/bin/bash
set -u
python3 - <<'PY'
import gzip
import json
import ssl
import urllib.request
url = "https://images.lightkeepers.moe/sim/characters.json.gz"
context = ssl._create_unverified_context()
req = urllib.request.Request(url, headers={"User-Agent": "read-only-review-probe"})
try:
with urllib.request.urlopen(req, context=context, timeout=30) as response:
raw = response.read()
status = response.status
except Exception as exc:
print(f"fetch_error={exc!r}")
raise SystemExit(0)
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
data = json.loads(raw)
characters = data.get("characters", data) if isinstance(data, dict) else {}
payloads = rows = present = missing = negative = 0
mixed_payloads = none_strength_payloads = all_strength_payloads = 0
min_strength = max_strength = None
missing_examples = []
negative_examples = []
for character_key, payload in characters.items():
weapons = payload.get("weapons") if isinstance(payload, dict) else None
if not isinstance(weapons, list) or not weapons:
continue
payloads += 1
local_present = local_missing = 0
for index, weapon in enumerate(weapons):
if not isinstance(weapon, dict):
continue
rows += 1
value = weapon.get("strength")
if value is not None:
local_present += 1
present += 1
if isinstance(value, (int, float)) and not isinstance(value, bool):
min_strength = value if min_strength is None else min(min_strength, value)
max_strength = value if max_strength is None else max(max_strength, value)
if value < 0:
negative += 1
if len(negative_examples) < 8:
negative_examples.append((character_key, index, weapon.get("key"), value))
else:
local_missing += 1
missing += 1
if len(missing_examples) < 8:
missing_examples.append((character_key, index, weapon.get("key")))
if local_present and local_missing:
mixed_payloads += 1
elif local_present:
all_strength_payloads += 1
elif local_missing:
none_strength_payloads += 1
print(f"http_status={status}")
print(f"characters={len(characters)}")
print(f"payloads_with_weapons={payloads}")
print(f"weapon_rows={rows}")
print(f"strength_present={present}")
print(f"strength_missing_or_null={missing}")
print(f"negative_strength={negative}")
print(f"strength_min={min_strength}")
print(f"strength_max={max_strength}")
print(f"payloads_all_strength={all_strength_payloads}")
print(f"payloads_mixed_strength={mixed_payloads}")
print(f"payloads_no_strength={none_strength_payloads}")
print(f"missing_examples={missing_examples}")
print(f"negative_examples={negative_examples}")
PYRepository: woopxwoop/lightkeepers
Length of output: 725
🏁 Script executed:
#!/bin/bash
set -u
python3 - <<'PY'
import functools
import gzip
import json
import ssl
import urllib.request
from pathlib import Path
url = "https://images.lightkeepers.moe/sim/characters.json.gz"
req = urllib.request.Request(url, headers={"User-Agent": "read-only-review-probe"})
with urllib.request.urlopen(req, context=ssl._create_unverified_context(), timeout=30) as response:
raw = response.read()
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
data = json.loads(raw)
characters = data.get("characters", data)
weapon_data = json.loads(Path("src/lib/data/weapons.json").read_text())
if isinstance(weapon_data, dict):
weapon_data = weapon_data.get("weapons", [])
stars = {}
for item in weapon_data:
if isinstance(item, dict):
key = item.get("key") or item.get("nameId") or item.get("name_id")
if key is not None:
stars[key] = item.get("stars", 0)
def compare_current(a, b):
ra, rb = stars.get(a.get("key"), 0), stars.get(b.get("key"), 0)
if ra != rb: return rb - ra
sa, sb = a.get("strength") if a.get("strength") is not None else 0, b.get("strength") if b.get("strength") is not None else 0
if sa != sb: return -1 if sb > sa else 1
if a["teams"] != b["teams"]: return b["teams"] - a["teams"]
return (a["key"] > b["key"]) - (a["key"] < b["key"])
def compare_missing_fallback(a, b):
ra, rb = stars.get(a.get("key"), 0), stars.get(b.get("key"), 0)
if ra != rb: return rb - ra
if a.get("strength") is not None and b.get("strength") is not None:
if a["strength"] != b["strength"]:
return -1 if b["strength"] > a["strength"] else 1
if a["teams"] != b["teams"]: return b["teams"] - a["teams"]
return (a["key"] > b["key"]) - (a["key"] < b["key"])
for character_key, payload in characters.items():
rows = payload.get("weapons", []) if isinstance(payload, dict) else []
if not rows or not any(r.get("strength") is None for r in rows):
continue
print(f"mixed_character={character_key}")
for row in rows:
print(json.dumps({
"key": row.get("key"),
"stars": stars.get(row.get("key"), 0),
"teams": row.get("teams"),
"strength": row.get("strength"),
}, sort_keys=True))
print("current_order=" + ",".join(r["key"] for r in sorted(rows, key=functools.cmp_to_key(compare_current))))
print("missing_fallback_order=" + ",".join(r["key"] for r in sorted(rows, key=functools.cmp_to_key(compare_missing_fallback))))
PYRepository: woopxwoop/lightkeepers
Length of output: 5335
Do not default a missing strength to 0.
CharacterIndex payloads can mix stamped and unstamped weapons. For example, TravelerPyro includes SacrificialSword with strength: 1 and FavoniusSword without strength, with equal team counts. Compare strength only when both rows provide it; otherwise compare team count. Current payload strengths are non-negative, so the negative-value case does not apply.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/character-builds.ts` around lines 130 - 143, The weapon comparator in
the sorting function should not coerce missing strength values to 0. Compare
strength only when both weapons provide it; otherwise continue to the teams
comparison, preserving the existing ordering for stamped rows and the current
non-negative strength behavior.
There was a problem hiding this comment.
Actionable comments posted: 23
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/routes/+page.svelte (1)
26-61: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winUse
siteAssetUrlfor feature banners.Import
siteAssetUrlfrom$lib/utilsand replace the six/site/*.webpliterals withsiteAssetUrl("team"),siteAssetUrl("abyss_banner"),siteAssetUrl("stygian_banner"),siteAssetUrl("heizou"),siteAssetUrl("kazuha"), andsiteAssetUrl("xiao"). The current CDN objects are available.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/`+page.svelte around lines 26 - 61, Import siteAssetUrl from $lib/utils in the page module and update each banner field in the features data, including rosterCard, to use siteAssetUrl with the corresponding asset name: team, abyss_banner, stygian_banner, heizou, kazuha, and xiao, removing the hard-coded CDN URLs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/app/character-summary.ts`:
- Around line 29-45: Update the fetch call in the character-summary request flow
to use the existing timeout pattern from upgrade-costs.ts, including
AbortSignal.timeout with the established CDN_FETCH_TIMEOUT_MS constant. Keep the
current response handling and pending cleanup unchanged so stalled requests
settle and later calls can retry.
In `@src/lib/calculator-goals-snapshot.ts`:
- Around line 73-89: Update fetchGoalsCloud to use an AbortController and abort
the GET request after the shared POST_TIMEOUT_MS duration, passing the
controller’s signal to fetch and cleaning up the timeout when the request
settles. Move POST_TIMEOUT_MS above fetchGoalsCloud so it is available there,
while preserving the existing null-return behavior for unauthorized, failed,
aborted, and other errors.
In `@src/lib/is-beta-character.test.ts`:
- Around line 11-14: Add test assertions in the existing “skips travelers and
released rows” case for isBetaCharacter covering the PlayerGirl prefix, and add
an assertion for an empty nameId to exercise the !nameId guard. Keep the
expected result false, matching the existing PlayerBoy and released-character
assertions.
In `@src/lib/planner-targets.ts`:
- Around line 75-85: Update resolveTalentTier to handle a missing
importance[slot] row by returning the corresponding
UPGRADE_DEFAULTS.characterTarget.talents fallback instead of dereferencing it;
preserve existing tier classification for present rows. Add a regression test
covering a malformed loadCharacterSummary response with teams > 0 and an omitted
talent slot.
In `@src/lib/server/request-validation.test.ts`:
- Around line 191-210: Extend the validation tests around requireCalculatorGoals
to cover requireTalentLevels: reject a talent value outside 1..MAX_TALENT,
reject a character missing the talents key, and reject a numeric-string talent
value. Use the existing character fixture and isBadRequest assertion pattern,
keeping the level and ascension cases unchanged.
In `@src/lib/server/request-validation.ts`:
- Around line 252-258: Update the weapon_id validation in the goals payload
handling to require that the positive integer also exists in the weapon catalog
before persistence. Reuse the existing catalog lookup or validation symbol
available in the request-validation flow, and throw the same 400
GOALS_PAYLOAD_ERROR for unknown IDs while preserving acceptance of valid catalog
weapons.
In `@src/lib/ui/components/CostPopover.svelte`:
- Around line 17-20: Update the explanatory text in CostPopover so the subject
and verb agree in the clause about constellation selectors receiving C1 for
free, using the plural form “constellation selectors get” while preserving the
rest of the meaning.
In `@src/lib/ui/components/NumberSliderField.svelte`:
- Around line 68-77: Update the range input in NumberSliderField to remove the
aria-valuemin and aria-valuemax overrides so its announced bounds come from the
native min and max attributes; preserve the existing label and value behavior.
- Around line 38-59: Update the number-input flow around emit and commitDraft so
non-empty typing is retained in draft without clamping or emitting on each
keystroke, then apply snap-to-step and effective bounds only when commitDraft
processes the completed draft. Keep empty-input handling intact, and update the
range input to call clampAndEmit so range changes continue clamping immediately.
In `@src/lib/ui/components/PickModal.svelte`:
- Around line 56-74: Update the open-state effect and its onKey handler in
PickModal so Tab and Shift+Tab cycle focus only among focusable elements inside
.pick-panel, wrapping from the last element to the first and vice versa.
Preserve the existing Escape behavior, focus restoration, and listener cleanup.
In `@src/lib/upgrade-costs-merge.test.ts`:
- Around line 128-140: Add assertions in the mergeUpgradeCostCatalogs test to
verify the exact ordering of merged.characters by name_id and merged.weapons by
id, while preserving the existing membership and precedence checks.
In `@src/lib/upgrade-costs-merge.ts`:
- Around line 14-18: Update sortedCharacters to sort name_id using a
locale-independent raw string comparison or an explicitly fixed locale, ensuring
identical ordering across build, server, and test environments while preserving
the existing copied-array behavior.
In `@src/lib/upgrade-costs.test.ts`:
- Line 61: Replace the boolean-wrapped assertion for result.exp with
assert.equal, preserving the expected value of 8_362_650 so failures report the
actual EXP total.
- Around line 23-36: Ensure the catalog consumed by loadCatalog is available
before upgrade-costs.test.ts runs: either commit the generated curves.json,
characters.json, and weapons.json under the expected output location, or add a
CI workflow step that generates them before pnpm test:unit.
In `@src/lib/upgrade-costs.ts`:
- Around line 62-71: Update minAscensionForLevel to return ascension 0 when
level is 1 or promotes is empty, before sorting or iterating the promote steps.
Preserve the existing sorted lookup and MAX_ASCENSION fallback for non-empty
tables and higher levels.
In `@src/lib/utils.ts`:
- Around line 513-514: Update the SIM_BASE and SIM_CONFIGS_BASE constants to
derive their URLs from the existing CDN_BASE constant instead of repeating the
API host literal, preserving their current /sim and /sim-configs paths.
In `@src/routes/api/calculator-goals/`+server.ts:
- Around line 22-51: Add the SQL migration that creates the
user_calculator_goals table with user_id, goals, and updated_at matching the
generated types, including required constraints and indexes as appropriate. Then
remove the interim GoalsRow and GoalsTable definitions and update goalsTable to
use the typed serverDb client directly, eliminating the unknown cast while
preserving its existing table access.
In `@src/routes/api/upgrade-costs/`+server.ts:
- Around line 48-60: Update tryFetchBetaJson to wrap the entire fetchWithTimeout
request, HTTP handling, and response JSON parsing in a try/catch; return null
for any thrown network, timeout, or parsing error while preserving the existing
status-based soft-fail behavior and warning for non-success HTTP responses.
- Around line 82-84: Update GET and the fetchCatalog path so beta CDN network,
timeout, and JSON parsing errors are caught and converted to null, allowing
Promise.all to succeed when live data is available. Ensure LRUCache.getOrSet
receives a resolving request rather than a rejected one, while preserving
live-data failures and existing successful caching behavior.
In `@src/routes/tools/planner/`+page.svelte:
- Around line 354-377: Update addCharacterWith and addWeaponWith to detect when
appendGoal reaches the existing MAX_CALCULATOR_GOALS cap, report the failure
through the goals panel’s addError, and return before calling beginConfigure;
preserve normal goal creation and dialog behavior when the append succeeds, and
import the limit constant from $lib/calculator-goals.
- Around line 413-424: Update the focus-management $effect around configCloseEl
and closeConfigure to handle Tab key presses by cycling focus among focusable
elements inside .config-panel, wrapping forward and backward at the boundaries
while preserving Escape behavior. Add the same focus trap to PickModal.svelte
only after implementing the shared behavior there.
In `@tests/smoke.spec.ts`:
- Around line 20-22: Add smoke-test coverage for the legacy routes in the test
list alongside the existing `/tools/*` entries, asserting each redirects with
HTTP 308 to its corresponding destination: `/abyss`, `/stygian`,
`/stygian/enemies`, `/stygian/enemies/[id]`, `/calculator`, and `/pulls`.
Preserve the current destination-page coverage.
In `@tests/stygian.spec.ts`:
- Line 9: Extend the test around page.goto("/tools/stygian") to cover the legacy
/stygian route, asserting that it redirects to /tools/stygian. Add one assertion
for the legacy route while preserving the existing destination-page coverage.
---
Outside diff comments:
In `@src/routes/`+page.svelte:
- Around line 26-61: Import siteAssetUrl from $lib/utils in the page module and
update each banner field in the features data, including rosterCard, to use
siteAssetUrl with the corresponding asset name: team, abyss_banner,
stygian_banner, heizou, kazuha, and xiao, removing the hard-coded CDN URLs.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 120c5323-cdc7-40c3-85e3-7100e0611690
⛔ Files ignored due to path filters (3)
package.jsonis excluded by none and included by nonepatch-notes/2026-08-11-roster-hotfix-and-patch-notes.mdis excluded by none and included by nonepatch-notes/README.mdis excluded by none and included by none
📒 Files selected for processing (90)
src/app.csssrc/lib/app/bootstrapClient.tssrc/lib/app/character-summary.tssrc/lib/app/upgrade-costs.tssrc/lib/asset-urls.tssrc/lib/calculator-goals-snapshot.test.tssrc/lib/calculator-goals-snapshot.tssrc/lib/calculator-goals.test.tssrc/lib/calculator-goals.tssrc/lib/character-builds.test.tssrc/lib/character-builds.tssrc/lib/character-filter.test.tssrc/lib/character-filter.tssrc/lib/definitions.tssrc/lib/is-beta-character.test.tssrc/lib/is-beta-character.tssrc/lib/patch-notes-seen.test.tssrc/lib/patch-notes.test.tssrc/lib/patch-notes.tssrc/lib/planner-targets.test.tssrc/lib/planner-targets.tssrc/lib/server/character-kit.tssrc/lib/server/request-validation.test.tssrc/lib/server/request-validation.tssrc/lib/server/stygian-enemies.tssrc/lib/solver.tssrc/lib/stores.tssrc/lib/types/calculator-goals.tssrc/lib/types/database.types.tssrc/lib/types/investment.tssrc/lib/types/upgrade-costs.tssrc/lib/ui/NavBar.sveltesrc/lib/ui/components/BrowseFlipCard.sveltesrc/lib/ui/components/CharacterIcon.sveltesrc/lib/ui/components/CharacterSearchSelect.sveltesrc/lib/ui/components/CostPopover.sveltesrc/lib/ui/components/HoverTooltip.sveltesrc/lib/ui/components/NumberSliderField.sveltesrc/lib/ui/components/PatchNotesPopup.sveltesrc/lib/ui/components/PickModal.sveltesrc/lib/upgrade-costs-merge.test.tssrc/lib/upgrade-costs-merge.tssrc/lib/upgrade-costs.test.tssrc/lib/upgrade-costs.tssrc/lib/upgrade-priority.test.tssrc/lib/upgrade-priority.tssrc/lib/utils.tssrc/routes/+layout.sveltesrc/routes/+page.sveltesrc/routes/abyss/+page.server.tssrc/routes/abyss/+page.sveltesrc/routes/api/calculator-goals/+server.tssrc/routes/api/character-summary/[key]/+server.tssrc/routes/api/upgrade-costs/+server.tssrc/routes/calculator/+page.server.tssrc/routes/calculator/+page.sveltesrc/routes/characters/[slug]/+page.server.tssrc/routes/characters/[slug]/+page.sveltesrc/routes/dev/ui/+page.sveltesrc/routes/patch-notes/+page.sveltesrc/routes/pulls/+page.server.tssrc/routes/pulls/+page.sveltesrc/routes/settings/panels/DisplayPanel.sveltesrc/routes/stygian/+page.server.tssrc/routes/stygian/+page.sveltesrc/routes/stygian/enemies/+page.server.tssrc/routes/stygian/enemies/+page.sveltesrc/routes/stygian/enemies/[id]/+page.server.tssrc/routes/stygian/enemies/[id]/+page.sveltesrc/routes/teams/configs/[slug]/+page.sveltesrc/routes/tools/abyss/+page.server.tssrc/routes/tools/abyss/+page.sveltesrc/routes/tools/calculator/+page.server.tssrc/routes/tools/calculator/+page.sveltesrc/routes/tools/planner/+page.sveltesrc/routes/tools/planner/+page.tssrc/routes/tools/pulls/+page.sveltesrc/routes/tools/pulls/+page.tssrc/routes/tools/stygian/+page.server.tssrc/routes/tools/stygian/+page.sveltesrc/routes/tools/stygian/enemies/+page.server.tssrc/routes/tools/stygian/enemies/+page.sveltesrc/routes/tools/stygian/enemies/[id]/+page.server.tssrc/routes/tools/stygian/enemies/[id]/+page.sveltetests/abyss.spec.tstests/global-setup.tstests/lazy-api.spec.tstests/pulls.spec.tstests/smoke.spec.tstests/stygian.spec.ts
💤 Files with no reviewable changes (2)
- src/routes/dev/ui/+page.svelte
- src/routes/characters/[slug]/+page.svelte
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
|
Summary by CodeRabbit