feat(leagues): league format with round-robin, Elo and admin access - #150
Conversation
|
Warning Review limit reached
Next review available in: 52 seconds You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: QUIET Plan: Pro Plus Run ID: 📒 Files selected for processing (18)
📝 WalkthroughWalkthroughSe incorpora una plataforma de ligas round-robin y Open Elo con persistencia, permisos, parejas, desafíos, clasificación, resultados, exploración y enlaces. También se añaden ranking, insignias, mejoras de perfil, controles compartidos y navegación contextual. ChangesLigas y partidas
Perfiles, insignias y navegación
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 28
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (17)
supabase/migrations/20260810200000_097_badge_updates.sql-140-168 (1)
140-168: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winDocumente que
streak_15deja de otorgarse y se retira de los perfiles.La función reconstruye
v_badgesdesdev_keysen cada ejecución. Al no incluirstreak_15, el bloqueDOfinal elimina esa insignia de todos los perfiles que ya la tenían.El encabezado del archivo lo indica, pero el efecto es una pérdida de datos visible para el usuario: una insignia ya conseguida desaparece del perfil y del showcase. Además, si alguna clave retirada figura en
profiles.badge_showcase, queda una referencia huérfana.Confirme que la retirada es intencional. Si lo es, considere depurar las referencias huérfanas en
badge_showcasedentro de esta misma migración.🤖 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 `@supabase/migrations/20260810200000_097_badge_updates.sql` around lines 140 - 168, Confirma que la retirada de streak_15 y de las claves obsoletas es intencional. Actualiza la reconstrucción de v_badges desde v_keys y el bloque DO final para depurar también las referencias retiradas de profiles.badge_showcase, evitando entradas huérfanas mientras se conserva el resto del showcase.src/hooks/useBadgeUnlocks.ts-16-41 (1)
16-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReinicie el estado conocido cuando cambia
sessionUserId.
knownKeysRefyqueueRefpersisten durante toda la vida del hook. El efecto solo depende destats. Si la sesión cambia a otro usuario sin que el árbol se desmonte, el hook conserva las claves del usuario anterior. Las insignias del nuevo usuario que no figuren en ese conjunto se celebran como recién desbloqueadas.Reinicie las referencias cuando cambia el identificador de sesión.
🛡️ Corrección propuesta
+ const lastUserRef = useRef<string | undefined>(undefined) + useEffect(() => { + if (lastUserRef.current !== sessionUserId) { + lastUserRef.current = sessionUserId + knownKeysRef.current = null + queueRef.current = [] + setCurrent(null) + } if (!stats) return const keys = stats.badges.map((b: PlayerBadge) => b.key)Añada
sessionUserIda las dependencias del efecto.🤖 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/hooks/useBadgeUnlocks.ts` around lines 16 - 41, Actualiza el efecto que procesa `stats` en el hook useBadgeUnlocks para incluir `sessionUserId` en sus dependencias y reiniciar knownKeysRef y queueRef cuando cambie el usuario autenticado. Mantén el comportamiento actual de detectar y encolar insignias nuevas para el usuario activo, evitando conservar datos de la sesión anterior.src/components/stats/TournamentPodiumSection.tsx-204-219 (1)
204-219: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse un único estado vacío cuando no existan medallas.
Con
total === 0yshowMedalCounts === true, la pantalla muestra un podio con ceros y el mismo mensaje vacío tres veces. Devuelva un único bloque vacío antes de renderizarVisualPodiumy las tres listas.🤖 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/components/stats/TournamentPodiumSection.tsx` around lines 204 - 219, Actualiza la condición de estado vacío en TournamentPodiumSection para devolver un único bloque cuando total === 0, independientemente de showMedalCounts. Elimina la dependencia de showMedalCounts en esa validación y evita renderizar VisualPodium y las tres PodiumList cuando no existan medallas.src/components/stats/BadgeShowcase.tsx-114-121 (1)
114-121: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winBloquee las opciones mientras se guarda.
Estas opciones siguen activas cuando
savingestrue. El usuario puede iniciar dos mutaciones debadge_showcaseantes de que termine la primera. El últimoUPDATErecibido puede sobrescribir la selección más reciente del usuario.Añada
disabled={saving}y rechacehandlePickcuandosavingseatrue.🤖 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/components/stats/BadgeShowcase.tsx` around lines 114 - 121, Disable the badge picker Pressable using disabled={saving}, and update handlePick to return immediately when saving is true. Ensure both the UI control and handler prevent starting another badge_showcase mutation until the current save completes.supabase/migrations/20260810230000_100_player_ranking.sql-67-69 (1)
67-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCompare la ciudad como un identificador normalizado.
ILIKE v_citytrata%y_como comodines. También excluye valores con espacios porquepr.cityno usaBTRIM. Una ciudad comoMadridpuede recibir un total y una posición local incorrectos.Use la misma comparación exacta y normalizada en ambas consultas.
Corrección propuesta
- AND (pr.city ILIKE v_city OR ps.user_id = p_user_id); + AND (lower(BTRIM(pr.city)) = lower(v_city) OR ps.user_id = p_user_id);Also applies to: 104-106
🤖 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 `@supabase/migrations/20260810230000_100_player_ranking.sql` around lines 67 - 69, Replace the city condition in both ranking queries with the same exact normalized comparison: trim surrounding whitespace from pr.city and compare it to the normalized v_city using equality rather than ILIKE, while preserving the ps.user_id = p_user_id fallback.src/components/stats/FormBadges.tsx-34-45 (1)
34-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAgrupe cada resultado en un único elemento accesible.
En el
Viewde cada ficha, useaccessibley unaccessibilityLabelque anuncieVictoriaoDerrota. AñadaÚltima partidacuandoisLatestsea verdadero. Mantenga la flecha fuera del árbol de accesibilidad.🤖 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/components/stats/FormBadges.tsx` around lines 34 - 45, Actualiza el View de cada ficha en FormBadges para que sea un único elemento accesible, usando accessible y un accessibilityLabel con “Victoria” o “Derrota”, añadiendo “Última partida” cuando isLatest sea verdadero. Excluye la flecha del árbol de accesibilidad mediante la propiedad correspondiente, sin alterar el contenido visual existente.src/services/leagues.service.ts-346-375 (1)
346-375: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse el nombre visible de la pareja en los desafíos.
listLeagueChallengestomaleague_pairs.namesin procesar.displayLeaguePairName(línea 566) solo usa el nombre almacenado cuandoname_is_customes verdadero; en el resto de casos compone el nombre con los miembros. Por eso un desafío puede mostrar un nombre distinto al de la misma pareja en la clasificación o en la lista de parejas.Recupere también los campos de miembros y reutilice
displayLeaguePairName.🤖 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/services/leagues.service.ts` around lines 346 - 375, Actualiza listLeagueChallenges para recuperar también los campos de miembros y name_is_custom de league_pairs, en lugar de seleccionar únicamente id y name. Construye challenger_name y challenged_name reutilizando displayLeaguePairName para cada pareja, manteniendo null cuando no exista una pareja asociada.src/app/l/[id].tsx-5-11 (1)
5-11: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winLas dos pantallas de redirección de liga son copias y comparten el mismo defecto.
useLocalSearchParamspuede devolver un array para un parámetro dinámico; en ese caso la plantilla/(tabs)/leagues/${id}genera una ruta no válida.
src/app/l/[id].tsx#L5-L11: normaliceidconArray.isArray(...) ? id[0] : idy redirija con{ pathname: '/(tabs)/leagues/[id]', params: { id } }.src/app/leagues/[id].tsx#L5-L11: aplique la misma normalización y la misma forma dehref, o extraiga un componente compartido de redirección y úselo en los dos archivos.🤖 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/app/l/`[id].tsx around lines 5 - 11, Normalice el parámetro id en ambas pantallas, src/app/l/[id].tsx#L5-L11 y src/app/leagues/[id].tsx#L5-L11, usando Array.isArray(...) ? id[0] : id antes de validar o redirigir. Reemplace la interpolación de la ruta por un href con pathname '/(tabs)/leagues/[id]' y params { id }, o extraiga y reutilice un componente compartido de redirección en ambos archivos.src/app/(tabs)/matches/[id].tsx-506-513 (1)
506-513: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winNormalice
idantes de limpiar el estado pendiente.
clearPendingMatchResultFromScoreboardcomparapending?.matchId === matchId. Este código pasaidsin normalizar. La línea 626 del mismo componente sí normaliza (Array.isArray(id) ? id[0] : id), porqueuseLocalSearchParamspuede devolver un array. Siidllega como array, la comparación falla y el estado pendiente no se limpia. El modal de resultado se vuelve a abrir al regresar a la pantalla.🐛 Cambio propuesto
+ const matchId = Array.isArray(id) ? id[0] : id + const closeMatchDetail = useCallback(() => { - clearPendingMatchResultFromScoreboard(id) + clearPendingMatchResultFromScoreboard(matchId) if (router.canGoBack()) { router.back() return } router.replace('/(tabs)/matches' as Href) - }, [router, id]) + }, [router, matchId])🤖 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/app/`(tabs)/matches/[id].tsx around lines 506 - 513, Normaliza el parámetro id antes de llamar a clearPendingMatchResultFromScoreboard dentro de closeMatchDetail, usando el mismo manejo de Array.isArray(id) ya aplicado en el componente. Pasa siempre el valor escalar resultante para que la comparación pending?.matchId === matchId funcione correctamente.src/components/leagues/AddLeaguePairModal.tsx-86-99 (1)
86-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winValida en el cliente que la pareja tenga al menos un jugador.
handleSubmitenvía el formulario aunque los dos slots estén vacíos. La restricciónleague_pairs_at_least_one_player, definida ensupabase/migrations/20260810120000_086_leagues.sql(líneas 85-88), rechaza ese caso. El consumidorsrc/app/(tabs)/leagues/[id].tsxmuestra el mensaje medianteshowAlert, por lo que el usuario ve el texto de error de la base de datos.Deshabilita el botón hasta que exista al menos un jugador.
🛡️ Corrección propuesta
+ const hasAnyPlayer = + playerAIsSelf || playerBIsSelf || playerAText.trim() !== '' || playerBText.trim() !== '' + const handleSubmit = async () => {- <Button title="Guardar pareja" onPress={() => void handleSubmit()} loading={loading} /> + <Button + title="Guardar pareja" + onPress={() => void handleSubmit()} + loading={loading} + disabled={!hasAnyPlayer} + />Also applies to: 191-191
🤖 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/components/leagues/AddLeaguePairModal.tsx` around lines 86 - 99, Update handleSubmit and the modal’s submit button state in AddLeaguePairModal so submission is allowed only when at least one player slot is populated. Disable the button when both playerAIsSelf/playerBIsSelf are false and their corresponding text fields are empty, while preserving the existing reset and error handling.src/app/(tabs)/leagues/edit/[id].tsx-171-175 (1)
171-175: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAñade
accessibilityLabelal botón de cierre.El contenido del
Pressablees el glifo✕. Los lectores de pantalla anuncian el carácter, no la acción.AddLeaguePairModal.tsx(línea 110) ya aplicaaccessibilityLabel="Cerrar"en el mismo control.♿ Corrección propuesta
<Pressable onPress={goBack} - accessibilityRole="button"> + accessibilityRole="button" + accessibilityLabel="Cerrar"> <Text style={s.closeX}>✕</Text> </Pressable>🤖 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/app/`(tabs)/leagues/edit/[id].tsx around lines 171 - 175, Añade accessibilityLabel="Cerrar" al Pressable de cierre en el flujo que usa goBack, igualando el patrón existente de AddLeaguePairModal y haciendo que los lectores de pantalla anuncien la acción en lugar del glifo ✕.src/components/leagues/EditLeaguePairModal.tsx-158-173 (1)
158-173: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winDeshabilita cada botón mientras la otra operación está en curso.
saveLoadingydeleteLoadingson independientes. El usuario puede pulsar «Eliminar pareja» mientras se guarda, o al contrario. Eso lanza dos mutaciones simultáneas sobre la misma pareja y el resultado final depende del orden de respuesta.🐛 Corrección propuesta
<Button title="Guardar cambios" onPress={() => void handleSubmit()} loading={saveLoading} + disabled={Boolean(deleteLoading)} /> {canDelete ? ( <Button title="Eliminar pareja" variant="outline" onPress={() => { void onDelete() }} loading={deleteLoading} + disabled={Boolean(saveLoading)} style={styles.deleteBtn} /> ) : null}🤖 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/components/leagues/EditLeaguePairModal.tsx` around lines 158 - 173, Actualiza los botones del modal alrededor de handleSubmit y onDelete para deshabilitar cada acción mientras cualquiera de las dos operaciones está en curso: usa saveLoading || deleteLoading como estado de disabled en ambos botones. Mantén sus estados loading individuales y la visibilidad condicionada por canDelete.supabase/migrations/20260811120000_103_refresh_player_stats_on_read.sql-63-86 (1)
63-86: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winUsa el mismo resultado confirmado en ambas funciones
La fórmula sí coincide:
k = 32,ROUND, piso de100y tratamiento de empates. Sin embargo,apply_match_elousa solo el resultadoconfirmedmás reciente, mientras_player_confirmed_match_rowsdevuelve todos. Si existen varios resultados confirmados para un partido, el rebuild aplica el Elo varias veces. Selecciona el mismo resultado más reciente o centraliza esta lógica.🤖 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 `@supabase/migrations/20260811120000_103_refresh_player_stats_on_read.sql` around lines 63 - 86, Actualiza _player_confirmed_match_rows para seleccionar únicamente el resultado confirmed más reciente por partido, igual que apply_match_elo, antes de calcular el Elo. Usa el mismo criterio de ordenamiento y deduplicación para que el rebuild procese cada partido una sola vez y conserve el tratamiento actual de empates y demás fórmulas.src/app/(tabs)/leagues/[id].tsx-197-197 (1)
197-197: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
openEndedno es booleano ynowMsqueda obsoleto.Dos puntos en este cálculo:
league.end_at &&devuelvenullo''cuando el valor es falsy. Por esoopenEndedno esboolean, ycanChallenge(línea 390) hereda ese tipo. Envuelva la expresión enBoolean(...).nowMssolo se actualiza al montar y al enfocar la pantalla. Si la fecha de fin se supera mientras el usuario permanece en la pantalla, el botón "Desafiar" continúa visible y la mutación falla en el servidor.🐛 Corrección propuesta para el tipo
- const openEnded = - isOpenEloFormat(league.format) && - league.end_at && - new Date(league.end_at).getTime() > nowMs + const openEnded = Boolean( + isOpenEloFormat(league.format) && + league.end_at && + new Date(league.end_at).getTime() > nowMs + )Also applies to: 260-263
🤖 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/app/`(tabs)/leagues/[id].tsx at line 197, Convierte el cálculo de openEnded en un booleano explícito usando Boolean(...) para que canChallenge conserve un tipo boolean. Actualiza nowMs mientras la pantalla permanezca enfocada, mediante un intervalo o temporizador con limpieza al desenfocarse/desmontarse, de modo que el estado se recalcule cuando league.end_at haya expirado y el botón “Desafiar” deje de mostrarse.src/app/(tabs)/leagues/[id].tsx-425-432 (1)
425-432: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winConservar la pareja seleccionada al desafiar.
onChallengesolo abre el modal. No transmite la pareja pulsada.ChallengeModal(línea 599) recibe la lista completachallengeOpponentsy obliga al usuario a elegir de nuevo el rival. Guarde la pareja pulsada en estado y pásela al modal como preselección.♻️ Corrección propuesta
- onChallenge={ - canChallenge - ? () => { - setChallengeModalOpen(true) - } - : undefined - } + onChallenge={ + canChallenge + ? () => { + setPreselectedOpponentId(pair.id) + setChallengeModalOpen(true) + } + : undefined + }🤖 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/app/`(tabs)/leagues/[id].tsx around lines 425 - 432, Actualiza el flujo de desafío en el componente de la liga para guardar en estado la pareja pulsada dentro de onChallenge, en lugar de solo abrir el modal. Pasa esa pareja como preselección a ChallengeModal, que actualmente recibe challengeOpponents, y conserva el comportamiento existente cuando no haya una selección inicial.src/components/leagues/EloRanking.tsx-36-38 (1)
36-38: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCorregir la pluralización de "partidas".
Cuando
row.playedes 1, el texto muestra "1 partidas". Ajuste el sustantivo según el valor.🐛 Corrección propuesta
<Text style={styles.meta}> - {row.played} partidas · {row.wins}V / {row.losses}D + {row.played} {row.played === 1 ? 'partida' : 'partidas'} · {row.wins}V /{' '} + {row.losses}D </Text>🤖 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/components/leagues/EloRanking.tsx` around lines 36 - 38, Ajusta el texto renderizado junto a `row.played` en `EloRanking` para mostrar “partida” cuando el valor sea 1 y “partidas” en cualquier otro caso, manteniendo sin cambios la información de victorias y derrotas.src/utils/leagueStandings.ts-73-79 (1)
73-79: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTratar el empate de juegos de forma explícita.
Si
teamAGames === teamBGames, la ramaelseasigna la victoria a la pareja B y la derrota a la pareja A. El tipoStandingMatchResultadmite valores iguales, por lo que un resultado con empate falsearía la clasificación. Descarte o gestione el empate de forma explícita.🐛 Corrección propuesta
if (r.teamAGames > r.teamBGames) { a.wins += 1 b.losses += 1 - } else { + } else if (r.teamBGames > r.teamAGames) { b.wins += 1 a.losses += 1 }🤖 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/utils/leagueStandings.ts` around lines 73 - 79, Handle the tie case explicitly in the standings update logic: when teamAGames equals teamBGames, do not award a win or loss to either team. Keep the existing win/loss updates only for strictly greater or lesser game counts in the surrounding league standings function.
🧹 Nitpick comments (25)
supabase/migrations/20260810180000_094_league_badges.sql (2)
32-65: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSustituya las cuatro subconsultas correlacionadas por una agregación única.
pair_statsejecuta cuatro subconsultas correlacionadas sobreconfirmedpor cada pareja. Con N parejas se recorreconfirmed4N veces. El bloqueDOde la línea 272 llama arecompute_player_stats_aggregatespara todos los usuarios deplayer_stats, por lo que el coste se multiplica durante la migración.Una sola agregación con
GROUP BYsobre las filas deconfirmedexpandidas por pareja produce el mismo resultado en un único recorrido.♻️ Estructura propuesta
per_pair AS ( SELECT c.league_id, c.pair_a AS pair_id, c.team_a_games AS gf, c.team_b_games AS ga FROM confirmed c UNION ALL SELECT c.league_id, c.pair_b AS pair_id, c.team_b_games AS gf, c.team_a_games AS ga FROM confirmed c ), agg AS ( SELECT league_id, pair_id, COUNT(*)::INT AS played, COUNT(*) FILTER (WHERE gf > ga)::INT AS wins, SUM(gf)::INT AS games_for, SUM(ga)::INT AS games_against FROM per_pair GROUP BY league_id, pair_id ), pair_stats AS ( SELECT lp.id AS pair_id, lp.league_id, COALESCE(a.played, 0) AS played, COALESCE(a.wins, 0) AS wins, COALESCE(a.games_for, 0) AS games_for, COALESCE(a.games_against, 0) AS games_against FROM public.league_pairs lp LEFT JOIN agg a ON a.pair_id = lp.id AND a.league_id = lp.league_id WHERE lp.league_id IN (SELECT DISTINCT up.league_id FROM user_pairs up) )🤖 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 `@supabase/migrations/20260810180000_094_league_badges.sql` around lines 32 - 65, Replace the four correlated subqueries in the pair_stats CTE with a single aggregation pipeline: expand confirmed rows into per_pair using UNION ALL for both sides, aggregate by league_id and pair_id in agg, then LEFT JOIN those results to league_pairs while preserving COALESCE defaults and the existing league filter. Keep the resulting played, wins, games_for, and games_against values equivalent to the current logic.
271-281: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoffEl recálculo masivo puede bloquear la migración.
El bloque
DOrecorre todas las filas deplayer_statsy ejecutarecompute_player_stats_aggregatesde forma secuencial dentro de una sola transacción. Cada llamada hace varias agregaciones sobrematches,match_participantsymatch_results. Con un volumen de usuarios elevado, la migración mantiene una transacción larga y retiene bloqueos de escritura sobreplayer_stats.Considere ejecutar el backfill fuera de la migración de esquema, o por lotes con confirmaciones intermedias.
🤖 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 `@supabase/migrations/20260810180000_094_league_badges.sql` around lines 271 - 281, Move the existing-player badge backfill out of the migration transaction instead of running the sequential DO block over all player_stats rows. Remove or replace the v_uid loop invoking recompute_player_stats_aggregates, and provide a separately executable batched process with intermediate commits so large datasets do not hold player_stats locks for the entire migration.supabase/migrations/20260810190000_096_hard_badges.sql (1)
15-25: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueEvalúe
_player_won_matchuna sola vez por fila.La expresión
public._player_won_match(mp.team, mr.team_a_games, mr.team_b_games)aparece en elCASEde la línea 16 y en el filtro de la línea 25. El planificador puede evaluarla dos veces por fila. Sobre un escaneo amplio, el coste se duplica sin necesidad.Calcule el valor en una subconsulta previa y filtre sobre la columna resultante.
🤖 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 `@supabase/migrations/20260810190000_096_hard_badges.sql` around lines 15 - 25, Calcule public._player_won_match una sola vez por fila mediante una subconsulta o CTE intermedia, exponiendo su resultado como una columna reutilizable. Actualice el CASE y el filtro de la consulta para referenciar esa columna calculada, manteniendo sin cambios la lógica de is_win y la exclusión de resultados NULL.src/hooks/useMatches.ts (2)
93-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUnifique los tres constructores de clave de exploración.
publicLeaguesExploreQueryKey,publicTournamentsExploreQueryKey(línea 78) ypublicMatchesExploreQueryKey(línea 135) derivan los mismos nueve campos y solo cambian la raíz. Un campo de filtro nuevo debe añadirse tres veces.Extraiga un helper que reciba la raíz y los filtros.
🤖 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/hooks/useMatches.ts` around lines 93 - 107, Unifica publicLeaguesExploreQueryKey, publicTournamentsExploreQueryKey y publicMatchesExploreQueryKey mediante un helper compartido que reciba la raíz y los filtros, construya una única clave con los nueve campos actuales y conserve sus valores por defecto y normalización. Haz que los tres constructores deleguen en ese helper usando únicamente sus respectivas raíces.
384-388: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winCentralice la invalidación de las consultas de liga.
Las claves literales coinciden actualmente con sus constructores. Use
invalidateLeagueQueries(queryClient, updated.league_id)desrc/hooks/useLeagues.tspara evitar duplicación y mantener también la invalidación deleague-challenges.🤖 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/hooks/useMatches.ts` around lines 384 - 388, Reemplace las tres llamadas literales a invalidateQueries dentro del bloque de updated.league_id en useMatches por invalidateLeagueQueries(queryClient, updated.league_id), reutilizando el helper de useLeagues.ts para centralizar también la invalidación de league-challenges.src/app/(tabs)/explore/index.tsx (1)
281-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winElimine la duplicación entre
tournamentFiltersyleagueFilters.Los dos objetos derivan los mismos nueve campos de
filters. Cualquier campo nuevo debe añadirse dos veces y es fácil que se desincronicen.Construya un único objeto derivado y reutilícelo en ambos hooks.
♻️ Cambio propuesto
- const leagueFilters: PublicLeaguesListFilters = useMemo( - () => ({ - search: filters.search, - city: filters.city, - status: filters.status, - hideCelebrated: filters.hideCelebrated, - startAfter: filters.startAfter, - startBefore: filters.startBefore, - minFreeSlots: filters.minFreeSlots, - contentType: filters.contentType, - visibility: filters.visibility ?? 'all', - }), - [filters] - ) + // `PublicLeaguesListFilters` y `PublicTournamentsListFilters` comparten la misma forma. + const leagueFilters: PublicLeaguesListFilters = tournamentFilters🤖 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/app/`(tabs)/explore/index.tsx around lines 281 - 301, Elimina la duplicación entre los objetos tournamentFilters y leagueFilters creando un único objeto derivado de filters con los nueve campos compartidos, incluido el valor predeterminado de visibility, y reutilízalo en usePublicLeaguesExplore y el hook de torneos. Actualiza las dependencias de useMemo para mantener el comportamiento existente.supabase/migrations/20260810120000_086_leagues.sql (1)
503-503: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueEl
CASEes redundante.Las dos ramas devuelven
p_end_at. Sustituye la expresión porp_end_at.♻️ Refactor propuesto
- CASE WHEN v_format = 'open_elo' THEN p_end_at ELSE p_end_at END, + p_end_at,🤖 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 `@supabase/migrations/20260810120000_086_leagues.sql` at line 503, Elimina el CASE redundante en la expresión de la migración y reemplázalo directamente por p_end_at, manteniendo sin cambios el resto de la lógica.supabase/migrations/20260810240000_101_admin_private_access.sql (1)
4-101: 🩺 Stability & Availability | 🔵 TrivialConsidera registrar los accesos de administrador a contenido privado.
Las tres funciones conceden lectura a los administradores sobre partidos, torneos y ligas privados sin contraseña. El repositorio ya define la tabla
audit_logs. Estas funciones sonSTABLE, por lo que no pueden escribir en ella.Si el cumplimiento normativo exige trazabilidad, registra el acceso en la capa de servicio o en los RPC de lectura que devuelven contenido privado, no en estos predicados.
🤖 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 `@supabase/migrations/20260810240000_101_admin_private_access.sql` around lines 4 - 101, No añadas escrituras de auditoría dentro de auth_can_read_match, auth_can_read_tournament ni auth_can_read_league, porque son funciones STABLE; mantén estos predicados como validaciones de acceso y registra los accesos administrativos al contenido privado en la capa de servicio o en los RPC de lectura que lo devuelven, reutilizando audit_logs.src/components/leagues/AddLeaguePairModal.tsx (1)
114-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUsa
KeyboardAwareScrollViewen lugar de combinarKeyboardAvoidingViewconautomaticallyAdjustKeyboardInsets.Los dos mecanismos ajustan el desplazamiento al aparecer el teclado. En iOS pueden acumularse y desplazar el contenido más de lo necesario. El repositorio ya define
@/components/ui/KeyboardAwareScrollView, que se usa ensrc/app/(tabs)/leagues/edit/[id].tsx(línea 10).Unifica el manejo del teclado con ese componente, o elimina uno de los dos mecanismos.
🤖 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/components/leagues/AddLeaguePairModal.tsx` around lines 114 - 120, Reemplaza la combinación de KeyboardAvoidingView y ScrollView en AddLeaguePairModal por el componente KeyboardAwareScrollView de `@/components/ui/KeyboardAwareScrollView`, siguiendo el uso existente en la pantalla de edición de ligas. Conserva los estilos, el contenido y la configuración de persistencia del teclado, y elimina los props y wrappers redundantes de ajuste del teclado.src/utils/elo.ts (1)
29-46: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDeriva
deltaBdedeltaApara conservar el Elo total.
Math.roundredondea los valores exactamente medios hacia arriba. Por esoeloDelta(a, b, 1, k)yeloDelta(b, a, 0, k)no siempre son opuestos: si el producto es±2.5, se obtiene3y-2, y el sistema crea 1 punto de Elo. En un ranking de suma cero esto acumula deriva.♻️ Cambio propuesto
const scoreA: 0 | 1 = aWon ? 1 : 0 const deltaA = eloDelta(ratingA, ratingB, scoreA, kFactor) - const deltaB = eloDelta(ratingB, ratingA, aWon ? 0 : 1, kFactor) + const deltaB = -deltaA🤖 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/utils/elo.ts` around lines 29 - 46, En computeEloUpdate, deriva deltaB a partir de deltaA para garantizar que ambos cambios sean opuestos y conservar el total de Elo; elimina el cálculo independiente mediante eloDelta para el jugador B, manteniendo sin cambios los valores anteriores y posteriores.src/utils/leagueStandings.test.ts (2)
30-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRenombra la variable y el título del test.
La variable
twocontiene tres parejas. El título del test es idéntico al de la línea 30 salvo el sufijo, lo que dificulta identificar cuál falla en el informe de Jest.♻️ Cambio propuesto
- it('uses games difference when h2h equal among tied', () => { - const two = [ + it('ranks by games difference when every pair has one win', () => { + const trio = [ { id: 'x', name: 'X' }, { id: 'y', name: 'Y' }, { id: 'z', name: 'Z' }, ] - const rows = computeLeagueStandings(two, [ + const rows = computeLeagueStandings(trio, [🤖 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/utils/leagueStandings.test.ts` around lines 30 - 36, Renombra la variable `two` en el test `uses games difference when h2h equal among tied` para reflejar que contiene tres parejas y actualiza el título para distinguirlo claramente del test de la línea 30, manteniendo la misma intención y aserciones.
20-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winEl test no comprueba el desempate que anuncia su nombre.
Las aserciones sólo verifican que hay tres filas y que todas tienen una victoria. El orden no se comprueba, por lo que el test pasaría aunque el desempate por diferencia de juegos estuviera roto. Con estos datos las diferencias son
b = +1,a = 0,c = -1.♻️ Aserción propuesta
expect(rows).toHaveLength(3) expect(new Set(rows.map((r) => r.wins))).toEqual(new Set([1])) + expect(rows.map((r) => r.pairId)).toEqual(['b', 'a', 'c']) + expect(rows.map((r) => r.gamesDiff)).toEqual([1, 0, -1])🤖 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/utils/leagueStandings.test.ts` around lines 20 - 28, Strengthen the test named “uses games difference when h2h equal in a cycle” by asserting the standings order reflects the games-difference tiebreak: team b first, team a second, and team c third. Keep the existing length and wins assertions if useful, but verify the ordered team identifiers rather than only aggregate wins.src/app/(tabs)/leagues/create.tsx (2)
234-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUnifica la presentación de errores en
showAlert.El archivo importa
showAlertde@/utils/alerty lo usa enrunDeletePair(línea 296). Los demás errores usanAlert.alertdereact-nativedirectamente. Esa mezcla evita la abstracción compartida y puede divergir en plataforma web. Sustituye las cuatro llamadas aAlert.alertporshowAlerty elimina la importación deAlert.Also applies to: 249-252, 265-268, 283-286
🤖 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/app/`(tabs)/leagues/create.tsx around lines 234 - 236, Reemplaza las cuatro llamadas a Alert.alert en los manejadores de error de create.tsx, incluida la del bloque catch mostrado y las secciones indicadas, por showAlert manteniendo sus títulos y mensajes. Elimina la importación de Alert desde react-native y conserva la importación existente de showAlert.
392-405: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueEl valor mostrado puede no coincidir con el estado del formulario.
value={field.value || defaultEndAt()}muestra una fecha calculada cuandoend_atestá vacío, pero no la escribe en el formulario. En ese casosuperRefinemarca «La liga abierta requiere fecha de fin» mientras el selector muestra una fecha válida. Asigna el valor por defecto consetValueal elegir el formatoOPEN_ELOen lugar de calcularlo en el render.🤖 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/app/`(tabs)/leagues/create.tsx around lines 392 - 405, Actualiza el flujo de selección de formato alrededor de `formatValue` para que, al elegir `LEAGUE_FORMAT.OPEN_ELO`, asigne la fecha de `defaultEndAt()` al campo `end_at` mediante `setValue` cuando esté vacío. Después, usa `field.value` directamente en `DateTimePicker` para mantener sincronizados el valor mostrado y el estado validado por `superRefine`.supabase/migrations/20260811120000_103_refresh_player_stats_on_read.sql (1)
134-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSustituye el parche por texto por una definición explícita de
get_player_stats.
pg_get_functiondef+replace+EXECUTEdepende de una cadena literal exacta dentro del cuerpo. Cualquier cambio de formato en una migración anterior (espacios, comentario intercalado, nombre de parámetro) provocaRAISE EXCEPTIONy bloquea el despliegue. Además, el cuerpo resultante no queda visible en el repositorio, lo que dificulta auditar una funciónSECURITY DEFINER.Declara
CREATE OR REPLACE FUNCTION public.get_player_stats(uuid)con el cuerpo completo y la llamada arefresh_player_stats, e incluye ahí mismo el atributoVOLATILEen lugar delALTER FUNCTIONposterior.🤖 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 `@supabase/migrations/20260811120000_103_refresh_player_stats_on_read.sql` around lines 134 - 154, Replace the dynamic pg_get_functiondef/replace/EXECUTE patch with an explicit CREATE OR REPLACE FUNCTION public.get_player_stats(uuid) definition containing the complete existing function body and calling public.refresh_player_stats. Preserve the function’s existing signature, return type, security attributes, and behavior, and declare VOLATILE in the function definition; remove the separate ALTER FUNCTION statement.src/components/leagues/LeaguePairCard.tsx (1)
60-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRenderiza el contenedor de acciones sólo cuando hay botones.
styles.actionsaplicamarginTop: 10de forma incondicional. En el paso 2 de la creación de ligas la tarjeta recibe sóloonEdit, por lo que el contenedor queda vacío y añade espacio inferior sin contenido.♻️ Cambio propuesto
- <View style={styles.actions}> - {onJoin && joinLabel ? ( + {(onJoin && joinLabel) || (onChallenge && challengeLabel) ? ( + <View style={styles.actions}> + {onJoin && joinLabel ? (Ajusta el cierre del bloque de forma acorde.
🤖 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/components/leagues/LeaguePairCard.tsx` around lines 60 - 78, Render the styles.actions container only when at least one action button is available, meaning the existing onJoin/joinLabel or onChallenge/challengeLabel conditions are satisfied. Update the surrounding JSX in LeaguePairCard so the empty container is not mounted while preserving both button render paths and their existing styles.src/components/ui/IconButton.tsx (1)
32-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAñade respuesta visual al pulsar.
Pressablerecibe un array de estilos estático. El botón no cambia de aspecto durante la pulsación, por lo que el usuario no recibe confirmación táctil visual. Este componente se reutiliza para editar parejas y añadir parejas, donde la respuesta importa.♻️ Cambio propuesto
<Pressable - style={[ + style={({ pressed }) => [ styles.base, variant === 'outline' && styles.outline, variant === 'primary' && styles.primary, disabled && styles.disabled, + pressed && !disabled && styles.pressed, style, ]}disabled: { opacity: 0.45 }, + pressed: { opacity: 0.6 }, })🤖 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/components/ui/IconButton.tsx` around lines 32 - 48, Update the Pressable in IconButton to provide a pressed-state visual style through its style callback, while preserving the existing base, variant, disabled, and custom styles. Add or reuse a pressed style so tapping the button visibly changes its appearance without altering onPress behavior.src/hooks/useLeagues.ts (1)
226-235: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winInvalida también las consultas de partidas al iniciar la liga.
startLeaguegenera los fixtures como partidas nuevas.onSuccessinvalida las claves de liga, el explorador público y el panel del usuario, pero no las claves['match', …]ni las listas de partidas ya cacheadas.useAcceptLeagueChallenge(líneas 260-263) sí lo hace. Aplica el mismo criterio para que los fixtures aparezcan de inmediato.♻️ Cambio propuesto
onSuccess: (_void, { leagueId }) => { invalidateLeagueQueries(queryClient, leagueId) invalidatePublicExplore(queryClient) invalidateMyMatchesDashboard(queryClient, sessionUserId) + queryClient.invalidateQueries({ queryKey: ['match'], exact: false }) },🤖 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/hooks/useLeagues.ts` around lines 226 - 235, Actualiza el callback onSuccess de la mutación que ejecuta startLeague para invalidar también las consultas de partidas y sus listas cacheadas, igual que useAcceptLeagueChallenge. Conserva las invalidaciones existentes de liga, explorador público y panel del usuario, reutilizando las mismas funciones o claves de invalidación disponibles.src/components/leagues/EloRanking.tsx (1)
12-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCorregir el comentario de la línea 12.
El comentario indica que las filas deben llegar ya ordenadas por
current_elodescendente. El componente aplica su propio orden en las líneas 14-18, por lo que esa precondición no existe. Actualice el comentario para describir el orden que aplica el componente: Elo, victorias y nombre.🤖 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/components/leagues/EloRanking.tsx` around lines 12 - 18, Actualiza el comentario de documentación sobre EloRanking para indicar que el componente ordena las filas por Elo descendente, luego por victorias descendentes y finalmente por nombre de pareja. Elimina la referencia a que las filas deban llegar previamente ordenadas.src/components/leagues/StandingsTable.tsx (1)
34-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAgrupar cada fila para los lectores de pantalla.
Cada fila se compone de celdas
Textindependientes. Un lector de pantalla las lee como valores sueltos, sin relación con las cabeceras "PJ", "PG", "PP" y "PTS". Añadaaccessibley unaccessibilityLabelpor fila que incluya posición, nombre de la pareja y estadísticas.♻️ Refactor propuesto
- <View key={row.pair_id} style={[styles.row, isPodium && styles.rowPodium]}> + <View + key={row.pair_id} + accessible + accessibilityLabel={`Posición ${row.rank}. ${row.pair_name}. ${row.played} jugados, ${row.wins} ganados, ${row.losses} perdidos.`} + style={[styles.row, isPodium && styles.rowPodium]}>🤖 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/components/leagues/StandingsTable.tsx` around lines 34 - 50, Actualiza el contenedor de cada fila dentro de rows.map en StandingsTable para marcarlo como accesible y proporcionar un accessibilityLabel único que agrupe la posición, el nombre de la pareja y las estadísticas PJ, PG, PP y PTS, usando los valores de row.rank, row.pair_name, row.played, row.wins, row.losses y row.points.src/app/(tabs)/leagues/[id].tsx (1)
222-233: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlinear el refetch en foco con
refreshAll.En la línea 229 se llama
challengesQ.refetch()sin comprobar el formato. EnrefreshAll(línea 271) el refetch de desafíos sí depende deisOpenEloFormat(league.format). Aplique la misma condición en ambos sitios para evitar peticiones innecesarias en ligas round-robin.🤖 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/app/`(tabs)/leagues/[id].tsx around lines 222 - 233, Update the focus effect around refetchLeague so challengesQ.refetch() runs only when isOpenEloFormat(league.format) is true, matching the condition already used by refreshAll; preserve the existing fullAccess guard and other refetch behavior.src/utils/leagueFixtures.test.ts (1)
4-26: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAñadir un aserto de unicidad de enfrentamientos.
Los tests validan el número de fixtures y las banderas de vuelta. No validan que cada pareja juegue contra cada otra exactamente una vez. Un generador con un emparejamiento duplicado y otro ausente pasaría estos tests. Añada un aserto sobre el conjunto de enfrentamientos.
♻️ Refactor propuesto
it('generates correct count for 4 pairs single round', () => { const ids = ['a', 'b', 'c', 'd'] const fixtures = generateRoundRobinFixtures(ids, false) expect(fixtures).toHaveLength(expectedMatchCount(4, false)) expect(fixtures.every((f) => !f.isSecondLeg)).toBe(true) + const keys = fixtures.map((f) => [f.pairAId, f.pairBId].sort().join('-')) + expect(new Set(keys).size).toBe(expectedMatchCount(4, false)) })🤖 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/utils/leagueFixtures.test.ts` around lines 4 - 26, Extend the tests around generateRoundRobinFixtures to assert matchup uniqueness: normalize each fixture’s pairAId/pairBId into an order-independent representation, then verify the set contains every distinct pair exactly once for single-round fixtures. Keep the existing count, second-leg, and bye assertions unchanged.src/lib/shareInvite.ts (1)
13-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTernarios anidados sin exhaustividad sobre
InviteShareKinden dos archivos. Los dos sitios resuelven el valor dekindcon ternarios anidados y dejan'league'en la rama por defecto. Si se añade un cuarto valor a la unión, ambos devolverán el resultado de liga sin error de compilación. La causa raíz compartida es la ausencia de una estructura exhaustiva indexada porInviteShareKind.
src/lib/shareInvite.ts#L13-L14: sustituya los ternarios por unRecord<InviteShareKind, string>con las etiquetaspartida,torneoyliga.src/components/ShareInviteButton.tsx#L26-L31: sustituya los ternarios por unRecord<InviteShareKind, (id: string) => string>con los tres constructores de URL, y obtenga la URL conbuilders[kind](id).🤖 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/shareInvite.ts` around lines 13 - 14, Replace the nested ternary in src/lib/shareInvite.ts lines 13-14 with a Record<InviteShareKind, string> containing the partida, torneo, and liga labels. Also replace the nested ternary in src/components/ShareInviteButton.tsx lines 26-31 with a Record<InviteShareKind, (id: string) => string> for the three URL builders, then obtain the URL through builders[kind](id) so both sites are exhaustive.src/components/leagues/ChallengeList.tsx (1)
25-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUsar una constante para el estado del desafío.
El literal
'pending'se compara en crudo. El repositorio ya centraliza estados ensrc/constants(por ejemploLEAGUE_STATUSyMATCH_VISIBILITY, importados ensrc/app/(tabs)/leagues/[id].tsx, línea 33). Exporte una constante de estado de desafío y úsela aquí. Así se evita la divergencia con el enum de base de datos.🤖 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/components/leagues/ChallengeList.tsx` at line 25, Replace the raw 'pending' comparison in ChallengeList with a shared exported challenge-status constant from the repository’s constants module, adding that constant if needed. Import and use the constant in the pending filter so it remains aligned with the database enum.src/lib/inviteLinks.ts (1)
18-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAñadir una prueba para el enlace de liga.
src/lib/inviteLinks.test.tsno cubrebuildLeagueHttpsInviteUrl. El caso deEXPO_PUBLIC_INVITE_HOSTausente ya está cubierto mediantebuildMatchHttpsInviteUrl.🤖 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/inviteLinks.ts` around lines 18 - 20, Añade una prueba en inviteLinks.test.ts para buildLeagueHttpsInviteUrl que verifique la URL HTTPS generada con un leagueId y un host configurado. Reutiliza el patrón de las pruebas existentes de buildMatchHttpsInviteUrl, sin duplicar la cobertura del caso de EXPO_PUBLIC_INVITE_HOST ausente.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: QUIET
Plan: Pro Plus
Run ID: 8bf36996-3cb4-4ff4-b573-e50c014dc451
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (88)
locales/es.jsonpackage.jsonsrc/app/(tabs)/_layout.tsxsrc/app/(tabs)/explore/index.tsxsrc/app/(tabs)/leaderboard/index.tsxsrc/app/(tabs)/leagues/[id].tsxsrc/app/(tabs)/leagues/create.tsxsrc/app/(tabs)/leagues/edit/[id].tsxsrc/app/(tabs)/matches/[id].tsxsrc/app/(tabs)/matches/create.tsxsrc/app/(tabs)/matches/edit/[id].tsxsrc/app/(tabs)/matches/index.tsxsrc/app/(tabs)/matches/scoreboard/[id].tsxsrc/app/(tabs)/profile/[userId].tsxsrc/app/(tabs)/profile/edit.tsxsrc/app/(tabs)/profile/index.tsxsrc/app/(tabs)/profile/stats/[userId].tsxsrc/app/(tabs)/tournaments/[id].tsxsrc/app/(tabs)/tournaments/create.tsxsrc/app/(tabs)/tournaments/edit/[id].tsxsrc/app/l/[id].tsxsrc/app/leagues/[id].tsxsrc/components/DeleteAccountModal.tsxsrc/components/ShareInviteButton.tsxsrc/components/leagues/AddLeaguePairModal.tsxsrc/components/leagues/CancelLeagueModal.tsxsrc/components/leagues/ChallengeList.tsxsrc/components/leagues/ChallengeModal.tsxsrc/components/leagues/EditLeaguePairModal.tsxsrc/components/leagues/EloRanking.tsxsrc/components/leagues/LeaguePairCard.tsxsrc/components/leagues/StandingsTable.tsxsrc/components/legal/LegalScreenLayout.tsxsrc/components/stats/BadgeList.tsxsrc/components/stats/BadgeShowcase.tsxsrc/components/stats/BadgeShowcaseSection.tsxsrc/components/stats/BadgeUnlockPopup.tsxsrc/components/stats/ELOBadge.tsxsrc/components/stats/FormBadges.tsxsrc/components/stats/ProfileStatsCard.tsxsrc/components/stats/RankingSection.tsxsrc/components/stats/TournamentPodiumSection.tsxsrc/components/tournaments/AddPairModal.tsxsrc/components/tournaments/PairCard.tsxsrc/components/ui/AddPairButton.tsxsrc/components/ui/CreateFab.tsxsrc/components/ui/IconButton.tsxsrc/constants/index.tssrc/hooks/useBadgeUnlocks.tssrc/hooks/useLeagues.tssrc/hooks/useMatches.tssrc/hooks/useResults.tssrc/hooks/useStats.tssrc/lib/inviteLinks.tssrc/lib/shareInvite.tssrc/services/leagues.service.tssrc/services/matches.service.tssrc/services/profiles.service.tssrc/services/stats.service.tssrc/types/database.types.tssrc/utils/elo.test.tssrc/utils/elo.tssrc/utils/exploreFilters.tssrc/utils/leagueDisplay.test.tssrc/utils/leagueDisplay.tssrc/utils/leagueFixtures.test.tssrc/utils/leagueFixtures.tssrc/utils/leagueForm.tssrc/utils/leagueStandings.test.tssrc/utils/leagueStandings.tssrc/utils/matchTeamNames.test.tssrc/utils/matchTeamNames.tssrc/utils/navigation.tssupabase/migrations/20260810120000_086_leagues.sqlsupabase/migrations/20260810130000_087_explore_exclude_league_matches.sqlsupabase/migrations/20260810140000_088_league_pair_member_names.sqlsupabase/migrations/20260810150000_089_league_finish_on_all_matches.sqlsupabase/migrations/20260810160000_090_match_team_display_names.sqlsupabase/migrations/20260810180000_094_league_badges.sqlsupabase/migrations/20260810181000_095_league_badges_apply.sqlsupabase/migrations/20260810190000_096_hard_badges.sqlsupabase/migrations/20260810200000_097_badge_updates.sqlsupabase/migrations/20260810210000_098_badge_showcase.sqlsupabase/migrations/20260810220000_099_badge_showcase_grants.sqlsupabase/migrations/20260810230000_100_player_ranking.sqlsupabase/migrations/20260810240000_101_admin_private_access.sqlsupabase/migrations/20260811000000_102_match_timers_tournament_league.sqlsupabase/migrations/20260811120000_103_refresh_player_stats_on_read.sql
| CREATE OR REPLACE FUNCTION public.recompute_player_stats_aggregates(p_user_id UUID) | ||
| RETURNS VOID | ||
| LANGUAGE plpgsql | ||
| SECURITY DEFINER | ||
| SET search_path = '' | ||
| AS $$ | ||
| DECLARE | ||
| r RECORD; | ||
| v_played INT := 0; | ||
| v_wins INT := 0; | ||
| v_losses INT := 0; | ||
| v_streak INT := 0; | ||
| v_best INT := 0; | ||
| v_run INT := 0; | ||
| v_won BOOLEAN; | ||
| v_form JSONB := '[]'::jsonb; | ||
| v_form_arr TEXT[] := ARRAY[]::TEXT[]; | ||
| v_t_won INT := 0; | ||
| v_t_finals INT := 0; | ||
| v_t_thirds INT := 0; | ||
| v_venues INT := 0; | ||
| v_badges JSONB := '[]'::jsonb; | ||
| v_existing JSONB; | ||
| v_now TIMESTAMPTZ := NOW(); | ||
| v_key TEXT; | ||
| v_keys TEXT[]; | ||
| v_nemesis_wins INT := 0; | ||
| v_league_gold INT := 0; | ||
| v_league_silver INT := 0; | ||
| v_league_bronze INT := 0; | ||
| v_league_part INT := 0; | ||
| v_broke_nine BOOLEAN := FALSE; | ||
| BEGIN | ||
| PERFORM public.ensure_player_stats_row(p_user_id); | ||
|
|
||
| FOR r IN | ||
| SELECT * FROM public._player_confirmed_match_rows(p_user_id) | ||
| LOOP | ||
| v_won := public._player_won_match(r.team, r.team_a_games, r.team_b_games); | ||
| IF v_won IS NULL THEN | ||
| CONTINUE; | ||
| END IF; | ||
|
|
||
| v_played := v_played + 1; | ||
| IF v_won THEN | ||
| v_wins := v_wins + 1; | ||
| IF v_run >= 0 THEN | ||
| v_run := v_run + 1; | ||
| ELSE | ||
| v_run := 1; | ||
| END IF; | ||
| IF v_run > v_best THEN | ||
| v_best := v_run; | ||
| END IF; | ||
| v_form_arr := array_append(v_form_arr, 'won'); | ||
| ELSE | ||
| v_losses := v_losses + 1; | ||
| IF v_run <= 0 THEN | ||
| v_run := v_run - 1; | ||
| ELSE | ||
| v_run := -1; | ||
| END IF; | ||
| v_form_arr := array_append(v_form_arr, 'lost'); | ||
| END IF; | ||
|
|
||
| IF r.tournament_id IS NOT NULL | ||
| AND r.tournament_round_size = 2 | ||
| AND NOT r.tournament_is_third_place THEN | ||
| v_t_finals := v_t_finals + 1; | ||
| IF r.tournament_winner_pair_id IS NOT NULL | ||
| AND ( | ||
| (r.team = 'A' AND r.tournament_winner_pair_id = r.tournament_pair_a_id) | ||
| OR (r.team = 'B' AND r.tournament_winner_pair_id = r.tournament_pair_b_id) | ||
| ) THEN | ||
| v_t_won := v_t_won + 1; | ||
| END IF; | ||
| END IF; | ||
|
|
||
| IF r.tournament_id IS NOT NULL | ||
| AND r.tournament_is_third_place | ||
| AND r.tournament_winner_pair_id IS NOT NULL | ||
| AND ( | ||
| (r.team = 'A' AND r.tournament_winner_pair_id = r.tournament_pair_a_id) | ||
| OR (r.team = 'B' AND r.tournament_winner_pair_id = r.tournament_pair_b_id) | ||
| ) THEN | ||
| v_t_thirds := v_t_thirds + 1; | ||
| END IF; | ||
| END LOOP; | ||
|
|
||
| v_streak := v_run; | ||
|
|
||
| IF cardinality(v_form_arr) > 5 THEN | ||
| v_form_arr := v_form_arr[(cardinality(v_form_arr) - 4):cardinality(v_form_arr)]; | ||
| END IF; | ||
| v_form := to_jsonb(v_form_arr); | ||
|
|
||
| SELECT COUNT(DISTINCT (city || '|' || COALESCE(place_text, '')))::INT | ||
| INTO v_venues | ||
| FROM public._player_confirmed_match_rows(p_user_id); | ||
|
|
||
| SELECT COALESCE(MAX(cnt), 0)::INT INTO v_nemesis_wins | ||
| FROM ( | ||
| SELECT opp.user_id, COUNT(*)::INT AS cnt | ||
| FROM public.match_participants me | ||
| JOIN public.match_participants opp | ||
| ON opp.match_id = me.match_id | ||
| AND opp.user_id <> me.user_id | ||
| AND opp.team <> me.team | ||
| AND opp.state = 'confirmed' | ||
| JOIN public.matches m ON m.id = me.match_id | ||
| JOIN public.match_results mr ON mr.match_id = m.id AND mr.status = 'confirmed' | ||
| WHERE me.user_id = p_user_id | ||
| AND me.state = 'confirmed' | ||
| AND m.status = 'finished' | ||
| AND COALESCE(m.tournament_is_bye, FALSE) = FALSE | ||
| AND public._player_won_match(me.team, mr.team_a_games, mr.team_b_games) IS TRUE | ||
| GROUP BY opp.user_id | ||
| ) s; | ||
|
|
||
| SELECT | ||
| COUNT(*) FILTER (WHERE lr.rank = 1)::INT, | ||
| COUNT(*) FILTER (WHERE lr.rank = 2)::INT, | ||
| COUNT(*) FILTER (WHERE lr.rank = 3)::INT, | ||
| COUNT(*)::INT | ||
| INTO v_league_gold, v_league_silver, v_league_bronze, v_league_part | ||
| FROM public._finished_league_ranks_for_user(p_user_id) lr; | ||
|
|
||
| v_broke_nine := public._player_broke_nine_win_streak(p_user_id); | ||
|
|
||
| SELECT badges INTO v_existing FROM public.player_stats WHERE user_id = p_user_id; | ||
| IF v_existing IS NULL THEN | ||
| v_existing := '[]'::jsonb; | ||
| END IF; | ||
|
|
||
| v_keys := ARRAY[]::TEXT[]; | ||
| IF v_wins >= 1 THEN v_keys := array_append(v_keys, 'first_win'); END IF; | ||
| IF v_wins >= 10 THEN v_keys := array_append(v_keys, 'wins_10'); END IF; | ||
| IF v_wins >= 25 THEN v_keys := array_append(v_keys, 'wins_25'); END IF; | ||
| IF v_wins >= 50 THEN v_keys := array_append(v_keys, 'wins_50'); END IF; | ||
| IF v_wins >= 100 THEN v_keys := array_append(v_keys, 'wins_100'); END IF; | ||
| IF v_t_won >= 1 THEN v_keys := array_append(v_keys, 'tournament_winner'); END IF; | ||
| IF v_t_finals >= 1 THEN v_keys := array_append(v_keys, 'tournament_finalist'); END IF; | ||
| IF v_league_gold >= 1 THEN v_keys := array_append(v_keys, 'league_winner'); END IF; | ||
| IF v_league_silver >= 1 THEN v_keys := array_append(v_keys, 'league_runner_up'); END IF; | ||
| IF (v_league_gold + v_league_silver + v_league_bronze) >= 1 THEN | ||
| v_keys := array_append(v_keys, 'league_podium'); | ||
| END IF; | ||
| IF v_league_part >= 3 THEN v_keys := array_append(v_keys, 'league_regular'); END IF; | ||
| IF v_best >= 5 THEN v_keys := array_append(v_keys, 'streak_5'); END IF; | ||
| IF v_best >= 10 THEN v_keys := array_append(v_keys, 'streak_10'); END IF; | ||
| IF v_broke_nine THEN v_keys := array_append(v_keys, 'streak_breaker'); END IF; | ||
| IF v_t_won >= 1 AND v_league_gold >= 1 THEN | ||
| v_keys := array_append(v_keys, 'double_champion'); | ||
| END IF; | ||
| IF v_played >= 50 THEN v_keys := array_append(v_keys, 'veteran_50'); END IF; | ||
| IF v_played >= 100 THEN v_keys := array_append(v_keys, 'veteran_100'); END IF; | ||
| IF v_venues >= 5 THEN v_keys := array_append(v_keys, 'explorer_5'); END IF; | ||
| IF v_venues >= 20 THEN v_keys := array_append(v_keys, 'explorer_20'); END IF; | ||
| IF v_nemesis_wins >= 5 THEN v_keys := array_append(v_keys, 'nemesis_confirmed'); END IF; | ||
| IF v_nemesis_wins >= 10 THEN v_keys := array_append(v_keys, 'rivalry_10'); END IF; | ||
| IF v_t_won >= 5 THEN v_keys := array_append(v_keys, 'crown_5'); END IF; | ||
| IF v_t_won >= 10 THEN v_keys := array_append(v_keys, 'crown_10'); END IF; | ||
| IF v_league_gold >= 3 THEN v_keys := array_append(v_keys, 'league_crown_3'); END IF; | ||
|
|
||
| v_badges := '[]'::jsonb; | ||
| FOREACH v_key IN ARRAY v_keys | ||
| LOOP | ||
| IF EXISTS ( | ||
| SELECT 1 FROM jsonb_array_elements(v_existing) e | ||
| WHERE e->>'key' = v_key | ||
| ) THEN | ||
| v_badges := v_badges || ( | ||
| SELECT e FROM jsonb_array_elements(v_existing) e WHERE e->>'key' = v_key LIMIT 1 | ||
| ); | ||
| ELSE | ||
| v_badges := v_badges || jsonb_build_array( | ||
| jsonb_build_object('key', v_key, 'earned_at', v_now) | ||
| ); | ||
| END IF; | ||
| END LOOP; | ||
|
|
||
| UPDATE public.player_stats | ||
| SET | ||
| matches_played = v_played, | ||
| wins = v_wins, | ||
| losses = v_losses, | ||
| current_streak = v_streak, | ||
| best_win_streak = v_best, | ||
| tournaments_won = v_t_won, | ||
| tournament_finals = v_t_finals, | ||
| tournament_thirds = v_t_thirds, | ||
| last_form = v_form, | ||
| badges = v_badges, | ||
| updated_at = v_now | ||
| WHERE user_id = p_user_id; | ||
| END; | ||
| $$; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Extraiga los cálculos comunes para evitar la tercera copia de la función.
recompute_player_stats_aggregates se reescribe completa en supabase/migrations/20260810180000_094_league_badges.sql, supabase/migrations/20260810190000_096_hard_badges.sql y en este archivo. Las tres versiones repiten el bucle de partidas, el recuento de sedes, el cálculo de némesis, la agregación de ligas y la fusión de insignias. Solo cambia la lista de claves.
Esta duplicación ya produjo divergencia: el defecto de concatenación con city nula existe en las tres copias.
Extraiga los cálculos en funciones auxiliares estables, por ejemplo _player_venue_count(uuid) y _player_max_wins_vs_rival(uuid). Las migraciones posteriores que solo añaden insignias pasarían a modificar únicamente el bloque de v_keys.
🤖 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 `@supabase/migrations/20260810200000_097_badge_updates.sql` around lines 6 -
202, Refactor recompute_player_stats_aggregates and the earlier duplicate
definitions so shared match aggregation, venue counting, nemesis calculation,
league aggregation, and badge merging live in stable helper functions, including
helpers such as _player_venue_count(uuid) and _player_max_wins_vs_rival(uuid).
Update each function to reuse those helpers and retain only its version-specific
v_keys additions, fixing venue counting through the shared implementation so
NULL city values are handled consistently.
| ALTER TABLE public.profiles | ||
| ADD COLUMN IF NOT EXISTS badge_showcase TEXT[] NOT NULL DEFAULT '{}'; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Imponga la integridad de badge_showcase en la base de datos.
El cliente filtra insignias obtenidas, pero un usuario autenticado puede actualizar su propio perfil mediante la API y omitir esa validación. Así puede mostrar insignias no obtenidas, duplicadas o más de tres insignias en perfiles visibles.
supabase/migrations/20260810210000_098_badge_showcase.sql#L4-L5: añada un trigger que rechace valores vacíos, duplicados, más de tres claves y claves que no existan enplayer_stats.badges.supabase/migrations/20260810220000_099_badge_showcase_grants.sql#L11-L17: conserve el permiso solo después de que la validación persistente proteja esta actualización directa.
📍 Affects 2 files
supabase/migrations/20260810210000_098_badge_showcase.sql#L4-L5(this comment)supabase/migrations/20260810220000_099_badge_showcase_grants.sql#L11-L17
🤖 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 `@supabase/migrations/20260810210000_098_badge_showcase.sql` around lines 4 -
5, En supabase/migrations/20260810210000_098_badge_showcase.sql, añade un
trigger de validación para badge_showcase que rechace valores vacíos,
duplicados, con más de tres claves o cuyas claves no existan en
player_stats.badges. En
supabase/migrations/20260810220000_099_badge_showcase_grants.sql, conserva el
permiso de actualización indicado, asegurándote de que la validación persistente
del trigger lo proteja.
c637dfa to
98f01b0
Compare
98f01b0 to
8327171
Compare
… podium (#142) * feat(stats): add player statistics, ELO ranking, and H2H match insights Introduce player_stats with ELO, win rate, rivalries, and badges backed by Supabase RPCs, plus profile stats screens, match H2H context, and an ELO leaderboard. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(stats): add tournament podium medals with named lists Show gold, silver and bronze medal counts on profile stats and list each tournament title in the stats detail screen. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(stats): unify league/tournament podiums and improve profile stats UX Merge podium medals across tournaments and leagues with source labels in detail view, surface win rate and medals at the top of profiles, and fix get_player_stats RPC volatility for PostgREST. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(matches): include phone_e164 in nested profile fallback select Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Satisfy CodeQL actions/missing-workflow-permissions by declaring least-privilege permissions on CI, Quality, Release, and Sentry health workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(leagues): add league format with round-robin, Elo, and UX polish Introduce leagues end-to-end (schema, services, screens, explore integration) with pair management, standings, and match grouping by jornadas. Includes profile visibility fixes, auto-finish when all fixtures complete, result screen name resolution, icon-based pair actions, and navigation from matches to their league. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(nav): return to match from league and to profile from history Preserve origin when closing match/league/tournament screens with X so league detail returns to the match and profile history returns to the profile. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(profile): polish stats UX, badge showcase, ranking and admin access Improve profile statistics with visual podium, featured badges, unlock popup, city/global ranking, safer account deletion, history-based navigation, and admin bypass for private password-protected events. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(stats): refresh ELO on profile load and fix match timers Keep league fixtures planned until played, extend tournament no-result timeout to 24h, and recalculate player stats when opening the profile. Also polish add-pair UX, ELO help copy, and empty podium messaging. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
8327171 to
0586dab
Compare
…ue SQL Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/(tabs)/explore/index.tsx (1)
474-481: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winEl mensaje de error sólo describe el fallo de partidas.
La condición de error ya cubre torneos y ligas. El cuerpo sigue mostrando
error, que pertenece auseInfinitePublicMatches. Si sólo falla la consulta de ligas o de torneos,erroresundefinedy la pantalla muestra «Error desconocido». El aviso sobre la migración009_list_public_matchestampoco corresponde a esos fallos.Extrae también
errorde las consultas de torneos y ligas, y muestra el primero disponible. Muestra la nota de migración sólo cuando falla la consulta de partidas.🐛 Corrección propuesta
const { data: leagues, isLoading: leaguesLoading, isError: leaguesIsError, + error: leaguesError, isRefetching: leaguesRefetching, refetch: refetchLeagues, } = usePublicLeaguesExplore(leagueFilters)+ const shownError = (showMatches && isError && error) || (showLeagues && leaguesError) || null <Text style={styles.errorMsg}> - {error instanceof Error ? error.message : 'Error desconocido'} + {shownError instanceof Error ? shownError.message : 'Error desconocido'} </Text> - <Text style={styles.errorHint}> - Si acabas de actualizar la app, aplica la migración Supabase `009_list_public_matches` en - tu proyecto. - </Text> + {showMatches && isError ? ( + <Text style={styles.errorHint}> + Si acabas de actualizar la app, aplica la migración Supabase `009_list_public_matches` + en tu proyecto. + </Text> + ) : null}Calcula
shownErrorfuera del JSX.🤖 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/app/`(tabs)/explore/index.tsx around lines 474 - 481, Actualiza el flujo de error del componente de exploración para extraer también los errores de las consultas de torneos y ligas, y calcula un `shownError` antes del JSX usando el primer error disponible entre partidas, torneos y ligas. Usa `shownError` en el mensaje mostrado y renderiza la nota de migración `009_list_public_matches` únicamente cuando falle la consulta de partidas.
🟡 Other comments (2)
src/app/(tabs)/leagues/[id].tsx-416-422 (1)
416-422: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winEl botón «Desafiar» de cada pareja no preselecciona el rival.
onChallengesólo abreChallengeModalsin indicar la pareja pulsada. El usuario debe volver a elegir el rival en la lista del modal. Guarda el id de la pareja pulsada y pásalo al modal como selección inicial.🐛 Corrección propuesta
+ const [challengeTargetPairId, setChallengeTargetPairId] = useState<string | null>(null)onChallenge={ canChallenge ? () => { + setChallengeTargetPairId(pair.id) setChallengeModalOpen(true) } : undefined }<ChallengeModal visible={challengeModalOpen} - onClose={() => setChallengeModalOpen(false)} + onClose={() => { + setChallengeModalOpen(false) + setChallengeTargetPairId(null) + }} opponents={challengeOpponents} + initialPairId={challengeTargetPairId}
ChallengeModalnecesita aceptar la nueva propinitialPairId.🤖 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/app/`(tabs)/leagues/[id].tsx around lines 416 - 422, Update the challenge handler around onChallenge to store the pressed pair’s id before opening the modal, then pass that id to ChallengeModal through its new initialPairId prop so the tapped opponent is preselected.src/services/leagues.service.ts-227-254 (1)
227-254: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winLimpia
end_atcuando el formato deja de seropen_elo.
updateLeaguesólo escribeend_atcuando el valor recibido es verdadero. Si el organizador cambia el formato deopen_eloasingle_round, la fila conserva la fecha de fin anterior. La pantalla de detalle sólo muestraend_atparaopen_elo, así que el dato queda persistido e invisible.🐛 Corrección propuesta
- if (data.end_at !== undefined && data.end_at) { - payload.end_at = startAtToTimestamptzIso(data.end_at) - } + if (data.end_at !== undefined) { + payload.end_at = data.end_at ? startAtToTimestamptzIso(data.end_at) : null + } + if (data.format !== undefined && data.format !== LEAGUE_FORMAT.OPEN_ELO) { + payload.end_at = null + }🤖 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/services/leagues.service.ts` around lines 227 - 254, Actualiza updateLeague para limpiar end_at cuando el formato deja de ser open_elo, asignando explícitamente null al payload en ese caso. Conserva la conversión existente para una fecha end_at proporcionada y asegúrate de que el cambio de open_elo a single_round elimine la fecha persistida.
🧹 Nitpick comments (8)
src/components/stats/BadgeShowcaseSection.tsx (1)
39-45: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winEl
catchanula la señal de error que esperaBadgeShowcase.
BadgeShowcase.handlePick(src/components/stats/BadgeShowcase.tsx, líneas 41-49) solo muestrapickerErrory mantiene el selector abierto sionChangerechaza. Aquí elcatchresuelve la promesa. El selector se cierra y la ranura vuelve al valor anterior sin explicación en la propia tarjeta, además delAlert.Elija una sola vía: relance el error tras el
Alert, o elimine elcatchy deje queBadgeShowcasemuestre el mensaje.♻️ Refactor propuesto
onChange={async (next) => { try { await updateProfile.mutateAsync({ badge_showcase: next }) } catch (err) { Alert.alert('Error', err instanceof Error ? err.message : 'No se pudo guardar el logro') + throw err } }}🤖 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/components/stats/BadgeShowcaseSection.tsx` around lines 39 - 45, Update the onChange handler in BadgeShowcaseSection to preserve BadgeShowcase.handlePick’s rejection-based error flow: either rethrow err after showing the Alert or remove the catch and rely on BadgeShowcase’s pickerError handling, choosing only one error-display path.src/utils/leagueFixtures.ts (1)
12-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winElimina
generateRoundRobinFixturesNo hay consumidores de producción de
generateRoundRobinFixturesni deexpectedMatchCount.startLeaguedelega la generación al RPCgenerate_league_fixtures. Además, ambas implementaciones ya difieren en la orientación de algunos partidos, por ejemplo con tres parejas. Conserva la utilidad solo si tendrá un consumidor; en ese caso, alinea ambas implementaciones y añade una prueba de paridad.🤖 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/utils/leagueFixtures.ts` around lines 12 - 66, Elimina la función generateRoundRobinFixtures y cualquier código asociado que no tenga consumidores de producción, incluyendo expectedMatchCount si también está sin uso. Mantén startLeague delegando exclusivamente en el RPC generate_league_fixtures; solo conserva la utilidad si se incorpora un consumidor real, alineando entonces su orientación con el RPC y añadiendo una prueba de paridad.src/app/(tabs)/leagues/[id].tsx (2)
319-324: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueIncluye los desafíos en el indicador de refresco.
refreshAllrecarga tambiénchallengesQen formato Open Elo, perorefreshingno observachallengesQ.isRefetching. El indicador desaparece antes de terminar la recarga.♻️ Refactor propuesto
<RefreshControl - refreshing={isRefetchingLeague || standingsQ.isRefetching || matchesQ.isRefetching} + refreshing={ + isRefetchingLeague || + standingsQ.isRefetching || + matchesQ.isRefetching || + challengesQ.isRefetching + } onRefresh={() => void refreshAll()} />🤖 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/app/`(tabs)/leagues/[id].tsx around lines 319 - 324, Incluye challengesQ.isRefetching en la expresión refreshing del RefreshControl junto con isRefetchingLeague, standingsQ.isRefetching y matchesQ.isRefetching, para que el indicador permanezca activo hasta que refreshAll termine de recargar los desafíos.
252-253: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
openEndedno es un booleano.La expresión devuelve
false,null, o la cadena deleague.end_at. Se usa después en condiciones de renderizado (líneas 386 y 432). Hoy las tres expresiones terminan en un operador ternario, así que no se renderiza texto suelto. Envuelve el valor enBoolean(...)para evitar una fuga de texto si alguien reordena esas condiciones.♻️ Refactor propuesto
- const openEnded = - isOpenEloFormat(league.format) && league.end_at && new Date(league.end_at).getTime() > nowMs + const openEnded = Boolean( + isOpenEloFormat(league.format) && league.end_at && new Date(league.end_at).getTime() > nowMs + )🤖 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/app/`(tabs)/leagues/[id].tsx around lines 252 - 253, Update the openEnded assignment near isOpenEloFormat so it always produces a boolean by wrapping the existing condition in Boolean(...). Preserve the current open-format and future end_at checks while ensuring downstream render conditions receive only true or false.src/utils/leagueDisplay.ts (1)
28-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUsa
Object.hasOwnen la comprobación de la etiqueta de formato.El operador
intambién encuentra propiedades heredadas del prototipo. Un valor como'constructor'devolvería una función en lugar de una cadena y rompería el renderizado del texto. Hoy el valor procede de la base de datos, así que el riesgo es teórico.♻️ Refactor propuesto
export function leagueFormatDisplay(format: string): string { - if (format in LEAGUE_FORMAT_LABELS) { + if (Object.hasOwn(LEAGUE_FORMAT_LABELS, format)) { return LEAGUE_FORMAT_LABELS[format as LeagueFormat] } return format }🤖 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/utils/leagueDisplay.ts` around lines 28 - 33, Actualiza la comprobación dentro de leagueFormatDisplay para usar Object.hasOwn sobre LEAGUE_FORMAT_LABELS en lugar del operador in, manteniendo el retorno de la etiqueta únicamente para claves propias y el fallback format sin cambios.src/app/(tabs)/leagues/create.tsx (1)
192-199: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueElimina el
useFocusEffectvacío.El callback no ejecuta ninguna acción y la función de limpieza está vacía. La dependencia
resettampoco se usa. El bloque sólo añade ruido y unuseCallbackinnecesario en cada foco.♻️ Refactor propuesto
- useFocusEffect( - useCallback(() => { - return () => { - // Keep current step/state while the screen stays mounted. - // Full reset happens after successful creation in `finish()`. - } - }, [reset]) - ) -Retira también el import de
useFocusEffectsi deja de usarse.🤖 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/app/`(tabs)/leagues/create.tsx around lines 192 - 199, Elimina el bloque vacío de useFocusEffect alrededor del callback de la pantalla de creación de ligas y retira el import de useFocusEffect si ya no tiene otros usos en el archivo; no modifiques el flujo de reset de finish().src/app/(tabs)/explore/index.tsx (1)
111-116: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSimplifica
leagueStatusTone.Tres ramas devuelven
'upcoming', incluida la rama por defecto. SóloIN_PROGRESScambia el tono.♻️ Refactor propuesto
function leagueStatusTone(league: LeagueRow): StatusDotTone { - if (league.status === LEAGUE_STATUS.IN_PROGRESS) return 'active' - if (league.status === LEAGUE_STATUS.REGISTRATION) return 'upcoming' - if (league.status === LEAGUE_STATUS.FINISHED) return 'upcoming' // tono neutro (gris) - return 'upcoming' + // Los demás estados usan el tono neutro. + return league.status === LEAGUE_STATUS.IN_PROGRESS ? 'active' : 'upcoming' }🤖 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/app/`(tabs)/explore/index.tsx around lines 111 - 116, Simplifica leagueStatusTone para devolver 'active' únicamente cuando league.status sea LEAGUE_STATUS.IN_PROGRESS y devolver 'upcoming' para cualquier otro estado. Elimina las ramas redundantes de REGISTRATION, FINISHED y el caso por defecto, manteniendo el mismo comportamiento.src/components/ShareInviteButton.tsx (1)
30-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLa rama final asume
leaguede forma implícita.Si
InviteShareKindgana un miembro nuevo, la cadena ternaria construye una URL de liga sin aviso del compilador. Usa un mapa por tipo para obtener comprobación exhaustiva.♻️ Refactor propuesto
+const INVITE_URL_BUILDERS: Record<InviteShareKind, (id: string) => string> = { + match: buildMatchHttpsInviteUrl, + tournament: buildTournamentHttpsInviteUrl, + league: buildLeagueHttpsInviteUrl, +}- const url = - kind === 'match' - ? buildMatchHttpsInviteUrl(id) - : kind === 'tournament' - ? buildTournamentHttpsInviteUrl(id) - : buildLeagueHttpsInviteUrl(id) + const url = INVITE_URL_BUILDERS[kind](id)🤖 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/components/ShareInviteButton.tsx` around lines 30 - 35, Replace the nested ternary in the URL construction with an exhaustive mapping keyed by InviteShareKind, so adding a new kind produces a compile-time error instead of implicitly using buildLeagueHttpsInviteUrl. Update the logic around buildMatchHttpsInviteUrl, buildTournamentHttpsInviteUrl, and buildLeagueHttpsInviteUrl while preserving the existing URL results for match, tournament, and league.
🤖 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/app/`(tabs)/matches/[id].tsx:
- Around line 769-777: Update canRecordAsLeagueReferee so league round-robin
matches represented by isPlannedLeagueMatch can be recorded before they become
in progress. Replace the isInProgress-only requirement with the same
planned-or-in-progress condition used by canSubmitResult and canOpenScoreboard,
while preserving all other eligibility checks.
In `@src/hooks/useBadgeUnlocks.ts`:
- Around line 16-42: Reset the badge-tracking state when sessionUserId changes
by adding an effect keyed to sessionUserId that clears knownKeysRef, empties
queueRef, and sets current to null. Ensure the existing stats effect then treats
the new account’s badges as its initial known set and does not carry over
unlocks from the previous session.
In `@src/services/leagues.service.ts`:
- Around line 480-505: Replace the `auth_can_read_league(id) as viewer_can_read`
selection in the leagues query with an RPC or view that exposes only leagues
visible to the current authenticated user, since the scalar function cannot be
invoked this way and must depend on `auth.uid()` at query time. Preserve the
existing league fields and ensure the exploration query no longer relies on a
generated column or invalid computed select expression.
In `@supabase/migrations/20260810120000_086_leagues.sql`:
- Around line 1479-1522: Remove the EXECUTE grant for authenticated on
process_league_lifecycle, leaving execution restricted to the role used by the
cron job. Follow the existing permission pattern of
process_match_state_transitions and keep the cancel_league grant unchanged.
- Around line 1555-1567: Actualiza el flujo que inserta en match_results y
modifica matches para invocar recompute_player_stats_for_match después de que el
UPDATE establezca status en 'finished'. Mantén la inserción y la finalización
existentes, y coloca la llamada antes de continuar con recalculate_league_elo y
maybe_finish_league.
In `@supabase/migrations/20260810140000_088_league_pair_member_names.sql`:
- Around line 144-165: Actualiza el flujo de creación alrededor de la función
que inserta en league_pairs para guardar primero la pareja y calcular después el
nombre automático con league_pair_slot_label, siguiendo el orden usado por
join_league_pair. Conserva el nombre personalizado sin recalcularlo, pero para
nombres automáticos actualiza la fila recién insertada con las etiquetas
resueltas después de que la pareja exista.
In `@supabase/migrations/20260810160000_090_match_team_display_names.sql`:
- Around line 183-195: Agrega la condición m.status IN ('planned',
'in_progress') a las cláusulas WHERE de ambos backfills de torneo que actualizan
team_a_name y team_b_name, igual que en los backfills de liga, manteniendo sin
cambios el resto de los criterios.
- Around line 167-181: Actualiza ambos bloques UPDATE para aplicar el patrón
sobre el nombre almacenado en la partida (team_a_name o team_b_name), no sobre
lp.name. Mantén las condiciones de league_pair, league_id y status, y conserva
league_pair_display_name(lp) como valor de reemplazo.
In `@supabase/migrations/20260810180000_094_league_badges.sql`:
- Around line 302-311: Elimine el bloque DO de
supabase/migrations/20260810180000_094_league_badges.sql#L302-L311 y de
supabase/migrations/20260810190000_096_hard_badges.sql#L301-L310; conserve un
único backfill de player_stats en
supabase/migrations/20260810200000_097_badge_updates.sql#L206-L215, o muévalo
fuera de la migración a un proceso por lotes si el volumen lo requiere.
In `@supabase/migrations/20260810190000_096_hard_badges.sql`:
- Around line 92-100: Remove the trailing comma after the streak_before CTE
definition in the migration, immediately before the main SELECT EXISTS query.
Keep the CTE definitions and the existing SELECT EXISTS logic unchanged.
- Around line 62-78: Remove the trailing comma after the streak_before CTE and
before the final SELECT EXISTS statement, while preserving the existing
win_streak_after calculation in streak_at_match.
In `@supabase/migrations/20260810200000_097_badge_updates.sql`:
- Around line 170-185: Actualiza la reconstrucción de v_badges en el bucle
FOREACH para conservar todas las insignias de v_existing, incluso cuando sus
claves no estén en v_keys. Retira únicamente las claves obsoletas mediante una
lista explícita del catálogo y evita que insignias permanentes como
league_winner o crown_5 desaparezcan durante un recálculo.
In `@supabase/migrations/20260810210000_098_badge_showcase.sql`:
- Around line 13-31: Corrige la consulta del UPDATE que construye `keys` para
deduplicar por clave (`k`) conservando únicamente la primera posición (`ord`) de
cada clave, en lugar de usar `DISTINCT k, ord`. Aplica `btrim` al valor antes de
filtrar y almacenar, rechazando valores nulos o vacíos tras recortar espacios, y
conserva el límite de tres claves y su orden original.
- Around line 39-44: Update the SECURITY DEFINER functions
validate_badge_showcase and get_viewable_user_profile to use SET search_path =
''. Within validate_badge_showcase, qualify unnest, btrim, length, cardinality,
jsonb_array_elements, and count with pg_catalog., while preserving the existing
auth.uid() qualification.
In `@supabase/migrations/20260811120000_103_refresh_player_stats_on_read.sql`:
- Around line 160-178: Replace the fragile pg_get_functiondef text-rewrite block
with an explicit CREATE OR REPLACE FUNCTION definition for
get_player_stats(uuid), preserving its existing signature, return type, security
attributes, and behavior while calling refresh_player_stats directly. Remove the
dynamic replacement and related string checks so the complete function
definition is versioned and readable.
- Around line 138-150: Elimina de refresh_player_stats la actualización de
profiles.badge_showcase basada en player_stats, evitando que get_player_stats
modifique perfiles ajenos durante una lectura. Conserva únicamente la lectura o
recálculo necesario de estadísticas y traslada la depuración del showcase al
flujo que recalcula las insignias después de confirmar un resultado, incluyendo
también el caso de rivales y compañeros gestionado por rebuild_player_elo.
---
Outside diff comments:
In `@src/app/`(tabs)/explore/index.tsx:
- Around line 474-481: Actualiza el flujo de error del componente de exploración
para extraer también los errores de las consultas de torneos y ligas, y calcula
un `shownError` antes del JSX usando el primer error disponible entre partidas,
torneos y ligas. Usa `shownError` en el mensaje mostrado y renderiza la nota de
migración `009_list_public_matches` únicamente cuando falle la consulta de
partidas.
---
Other comments:
In `@src/app/`(tabs)/leagues/[id].tsx:
- Around line 416-422: Update the challenge handler around onChallenge to store
the pressed pair’s id before opening the modal, then pass that id to
ChallengeModal through its new initialPairId prop so the tapped opponent is
preselected.
In `@src/services/leagues.service.ts`:
- Around line 227-254: Actualiza updateLeague para limpiar end_at cuando el
formato deja de ser open_elo, asignando explícitamente null al payload en ese
caso. Conserva la conversión existente para una fecha end_at proporcionada y
asegúrate de que el cambio de open_elo a single_round elimine la fecha
persistida.
---
Nitpick comments:
In `@src/app/`(tabs)/explore/index.tsx:
- Around line 111-116: Simplifica leagueStatusTone para devolver 'active'
únicamente cuando league.status sea LEAGUE_STATUS.IN_PROGRESS y devolver
'upcoming' para cualquier otro estado. Elimina las ramas redundantes de
REGISTRATION, FINISHED y el caso por defecto, manteniendo el mismo
comportamiento.
In `@src/app/`(tabs)/leagues/[id].tsx:
- Around line 319-324: Incluye challengesQ.isRefetching en la expresión
refreshing del RefreshControl junto con isRefetchingLeague,
standingsQ.isRefetching y matchesQ.isRefetching, para que el indicador
permanezca activo hasta que refreshAll termine de recargar los desafíos.
- Around line 252-253: Update the openEnded assignment near isOpenEloFormat so
it always produces a boolean by wrapping the existing condition in Boolean(...).
Preserve the current open-format and future end_at checks while ensuring
downstream render conditions receive only true or false.
In `@src/app/`(tabs)/leagues/create.tsx:
- Around line 192-199: Elimina el bloque vacío de useFocusEffect alrededor del
callback de la pantalla de creación de ligas y retira el import de
useFocusEffect si ya no tiene otros usos en el archivo; no modifiques el flujo
de reset de finish().
In `@src/components/ShareInviteButton.tsx`:
- Around line 30-35: Replace the nested ternary in the URL construction with an
exhaustive mapping keyed by InviteShareKind, so adding a new kind produces a
compile-time error instead of implicitly using buildLeagueHttpsInviteUrl. Update
the logic around buildMatchHttpsInviteUrl, buildTournamentHttpsInviteUrl, and
buildLeagueHttpsInviteUrl while preserving the existing URL results for match,
tournament, and league.
In `@src/components/stats/BadgeShowcaseSection.tsx`:
- Around line 39-45: Update the onChange handler in BadgeShowcaseSection to
preserve BadgeShowcase.handlePick’s rejection-based error flow: either rethrow
err after showing the Alert or remove the catch and rely on BadgeShowcase’s
pickerError handling, choosing only one error-display path.
In `@src/utils/leagueDisplay.ts`:
- Around line 28-33: Actualiza la comprobación dentro de leagueFormatDisplay
para usar Object.hasOwn sobre LEAGUE_FORMAT_LABELS en lugar del operador in,
manteniendo el retorno de la etiqueta únicamente para claves propias y el
fallback format sin cambios.
In `@src/utils/leagueFixtures.ts`:
- Around line 12-66: Elimina la función generateRoundRobinFixtures y cualquier
código asociado que no tenga consumidores de producción, incluyendo
expectedMatchCount si también está sin uso. Mantén startLeague delegando
exclusivamente en el RPC generate_league_fixtures; solo conserva la utilidad si
se incorpora un consumidor real, alineando entonces su orientación con el RPC y
añadiendo una prueba de paridad.
🪄 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: Organization UI
Review profile: QUIET
Plan: Pro Plus
Run ID: 856152e0-adc9-4183-8508-a7d097bc46e9
📒 Files selected for processing (26)
src/app/(tabs)/explore/index.tsxsrc/app/(tabs)/leagues/[id].tsxsrc/app/(tabs)/leagues/create.tsxsrc/app/(tabs)/matches/[id].tsxsrc/app/(tabs)/matches/index.tsxsrc/components/ShareInviteButton.tsxsrc/components/leagues/StandingsTable.tsxsrc/components/stats/BadgeShowcaseSection.tsxsrc/components/stats/BadgeUnlockPopup.tsxsrc/components/stats/ProfileStatsCard.tsxsrc/components/stats/RankingSection.tsxsrc/hooks/useBadgeUnlocks.tssrc/services/leagues.service.tssrc/services/stats.service.tssrc/types/database.types.tssrc/utils/leagueDisplay.tssrc/utils/leagueFixtures.tssupabase/migrations/20260810120000_086_leagues.sqlsupabase/migrations/20260810140000_088_league_pair_member_names.sqlsupabase/migrations/20260810160000_090_match_team_display_names.sqlsupabase/migrations/20260810180000_094_league_badges.sqlsupabase/migrations/20260810190000_096_hard_badges.sqlsupabase/migrations/20260810200000_097_badge_updates.sqlsupabase/migrations/20260810210000_098_badge_showcase.sqlsupabase/migrations/20260811000000_102_match_timers_tournament_league.sqlsupabase/migrations/20260811120000_103_refresh_player_stats_on_read.sql
| let query = supabase | ||
| .from('leagues') | ||
| .select( | ||
| [ | ||
| 'id', | ||
| 'title', | ||
| 'description', | ||
| 'notes', | ||
| 'start_at', | ||
| 'end_at', | ||
| 'city', | ||
| 'place_defined', | ||
| 'place_text', | ||
| 'duration_target_games', | ||
| 'visibility', | ||
| 'location_privacy', | ||
| 'format', | ||
| 'status', | ||
| 'creator_id', | ||
| 'fixtures_generated_at', | ||
| 'elo_initial', | ||
| 'elo_k_factor', | ||
| // Helper to check access to private leagues. | ||
| 'auth_can_read_league(id) as viewer_can_read', | ||
| ].join(',') | ||
| ) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- definición de auth_can_read_league ---'
rg -n -C6 'auth_can_read_league' supabase/migrations -g '*.sql'
echo '--- otros usos de sintaxis func(...) as alias en select() ---'
rg -n -C3 "select\(" src/services -g '*.ts' | rg -n -C3 '\) as '
echo '--- vistas o columnas generadas para exploración de ligas ---'
rg -n -C4 'create (or replace )?view|generated always as' supabase/migrations -g '*league*'Repository: Japegomez/jugaMUS
Length of output: 19469
🌐 Web query:
PostgREST select parameter支持 function call computed field syntax
💡 Result:
In PostgREST, computed fields are implemented as PostgreSQL functions that take a table type as an argument [1][2]. When you want to include these computed fields in a response, you simply list their name in the select parameter, just as you would with a regular column [1][2]. For example, if you have a function named full_name that takes the people table as an argument: CREATE FUNCTION full_name(people) RETURNS text AS $$ SELECT $1.first_name || ' ' ||
Citations:
- 1: https://postgrest.org/en/v11/references/api/computed_fields.html
- 2: https://docs.postgrest.org/en/v12/references/api/computed_fields.html
- 3: Confused on computed columns and relationships PostgREST/postgrest#2475
- 4: 2 simular computed columns / fields for different roles behaving differently PostgREST/postgrest#4244
- 5: https://docs.postgrest.org/en/stable/references/api/functions.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- servicio: consulta y filtrado de ligas ---'
sed -n '450,565p' src/services/leagues.service.ts
echo '--- función de autorización ---'
sed -n '65,105p' supabase/migrations/20260810240000_101_admin_private_access.sql
sed -n '165,205p' supabase/migrations/20260810120000_086_leagues.sql
echo '--- usos de viewer_can_read ---'
rg -n -C4 'viewer_can_read|auth_can_read_league\(id\)' src supabase -g '*.ts' -g '*.sql'
echo '--- comprobación estructural de la cadena select ---'
python3 - <<'PY'
from pathlib import Path
import re
path = Path("src/services/leagues.service.ts")
text = path.read_text()
match = re.search(r"\.select\(\s*\[\s*(.*?)\s*\]\.join\(['\"][,]?['\"]\)", text, re.S)
if not match:
raise SystemExit("No se encontró la lista select de leagues.service.ts")
items = match.group(1)
target = "auth_can_read_league(id) as viewer_can_read"
print("target_present:", target in items)
print("target_is_scalar_call_alias:", bool(re.search(r"\b\w+\([^)]*\)\s+as\s+\w+", items)))
print("select_items:")
for line in items.splitlines():
if "auth_can_read_league" in line or "::" in line or " as " in line:
print(line.strip())
PYRepository: Japegomez/jugaMUS
Length of output: 8418
Corrige el campo calculado de acceso
auth_can_read_league(uuid) existe, pero es una función escalar y no puede usarse con auth_can_read_league(id) as viewer_can_read en select. La consulta puede devolver un error 400 y dejar vacía la exploración. Usa un RPC o una vista que exponga las ligas visibles; una columna generada no es adecuada porque el resultado depende de auth.uid().
🤖 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/services/leagues.service.ts` around lines 480 - 505, Replace the
`auth_can_read_league(id) as viewer_can_read` selection in the leagues query
with an RPC or view that exposes only leagues visible to the current
authenticated user, since the scalar function cannot be invoked this way and
must depend on `auth.uid()` at query time. Preserve the existing league fields
and ensure the exploration query no longer relies on a generated column or
invalid computed select expression.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Tighten league referee recording, badge unlock resets, explore errors, and SQL migrations; drop unused client round-robin fixtures helper. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Test plan
Summary by CodeRabbit
Nuevas funciones
Mejoras