Skip to content

Tournament detail: round carousel + standings pagination - #129

Open
Franc3s-bot wants to merge 186 commits into
Francespo:mainfrom
Franc3s-bot:heroic-cat
Open

Tournament detail: round carousel + standings pagination#129
Franc3s-bot wants to merge 186 commits into
Francespo:mainfrom
Franc3s-bot:heroic-cat

Conversation

@Franc3s-bot

Copy link
Copy Markdown
Contributor

Summary

Restructure the tournament detail page to fix excessive page length in tournaments with many participants/matches.

Match rounds → navigable carousel

  • Shows one round at a time instead of all rounds stacked vertically
  • Left/right arrow buttons to navigate between rounds
  • Dot indicators at the bottom for direct round access
  • Box is viewport-constrained (max-height: 100vh - 120px) with internal scroll

Standings → paginated

  • Both Cut Standings and Swiss Standings show 10 entries per page
  • Prev/Next buttons at the bottom when >10 players
  • Page indicator in the header (e.g. "Page 1 / 3")

No backend changes needed — all data is already loaded; only the rendering was restructured.

Francespo and others added 30 commits March 4, 2026 19:28
…cespo#46)

Fixed the backend import issues with faction char and improved the
startup script to avoid opening new windows and handle dynamic ports.
…provements

# Conflicts:
#	backend/analytics/factions.py
#	backend/analytics/ships.py
… with search bar

- Add GET /api/tournaments/locations endpoint returning distinct locations
- Split location filter into 3 dynamic sub-filters (Continent, Country, City)
- Countries update based on selected Continents, Cities based on selected Countries
- Add mini search bar to filter location entries
- Remove duplicate Unknown entries from continent list
…tructure

- Move ShipChassisFilter below Faction on ships, cards, lists, squadrons pages
- Remove accordion expand/collapse; make it a flat static section
- Ship icons in filter inherit text color (uncolored)
- Faction symbols remain colored next to ship names
- Respects specificity hierarchy: Sort By > Faction > Ship Chassis
Closes Francespo#37. This PR fixes three critical bugs in the analytics modules:

1. **Tournament filters ignored**: Added centralized location filtering
via �pply_tournament_filters() and updated SQL filters.
2. **Lists count 0**: Added lists aggregation to core.py.
3. **Frontend filters ignored**: Merged main, resolved UI conflicts, and
fixed a Svelte a11y bug (�11y_consider_explicit_label) that caused a 500
error.
# Conflicts:
#	backend/analytics/factions.py
#	backend/analytics/ships.py
…provements

# Conflicts:
#	backend/analytics/ships.py
#	backend/api/ships.py
#	frontend/src/routes/lists/+page.svelte
#	frontend/src/routes/ships/+page.svelte
…ion bugs (Francespo#45)

Closes Francespo#42.

## Technical Changes
- Replaced SSR data loading in `+page.ts` with reactive client-side
`$effect` fetching to resolve route navigation desync and crashes.
- Replaced hardcoded 'XWA / Analysis Mode' title with the
`<ContentSourceToggle />` component in the header.
- Implemented reactive data fetching based on the global
`filters.dataSource` state to seamlessly switch between XWA and Legacy
data sets without requiring a page reload.
Closes Francespo#38

## Summary

Adds the Ships / Chassis page with filtering capabilities and fixes the
hover scaling behavior on ship cards.

## Changes

### Backend
- **New** `backend/routers/ships.py`: FastAPI router exposing `GET
/api/ships` with optional `faction` and `source` query params.
- **Modified** `backend/main.py`: Registers the ships router under
`/api` prefix.
- **Fixed** `backend/utils/xwing_data/core.py`: Corrected `ROOT_DIR`
path (`parents[4]` to `parents[3]`).

### Frontend
- **New** `frontend/src/routes/ships/+page.svelte`: Ships page with
chassis filter dropdown, faction filter, responsive card grid, and
**hover scale on entire card container** (not inner icon).
- **Modified** `frontend/src/routes/+layout.svelte`: Added navigation
bar with Ships link.
- **Modified** `frontend/vite.config.js`: API proxy to FastAPI backend.

## Technical Rationale

The hover scaling fix places `transform: scale(1.06)` on the outer
`.ship-card` container rather than inner icon wrappers, ensuring the
entire card elevates uniformly on hover.
# Conflicts:
#	frontend/src/routes/squadrons/+page.svelte
Closes Francespo#43.

- Alphabetized SortSelector options list.
- Changed SVG arrows to use distinct upward/downward pointing icons.
- Updated Squadrons page to use SortSelector component.
- Used getFactionLabel instead of raw faction key in SquadronRowCard.
Removes remaining garbage files and logs from main.
Closes Francespo#44. Identified 6 missing ship icons in xwing-miniatures.css by
comparing with JSON data. Added mappings for ieininterceptor,
delta7baethersprite, cr90corelliancorvette, iesebomber, and
upsilonclassshuttle. Trident remains without icon as per user feedback.

---------

Co-authored-by: Francesco Esposito <95753785+Francespo@users.noreply.github.com>
…rancespo#57)

## Description
This PR fixes the accidental creation of a local font directory in the
frontend and establishes a single source of truth using a symlink.

- Migrated centered xwing-miniatures.css to
external_data/xwing-miniatures-font/dist/.
- Removed local rontend/static/fonts/xwing-miniatures-font/ directory.
- Created a directory junction (symlink) pointing to the source in
external_data.
- Squashed branch history into a single clean commit.

Co-authored-by: Francesco Esposito <95753785+Francespo@users.noreply.github.com>
Closes Francespo#52. \n\n### Changes Made:\n- **Standardized Labeling**: All
unknown tournament formats and scenarios now use the label 'Unknown'.\n-
**Fixed UNK Badges**: Verified that unknown formats correctly display
the 'UNK' badge in the Tournament Browser list.\n- **Sidebar UI
Polish**:\n - Restored horizontal dividers between all major filter
sections.\n - Fixed double divider issue when Active Filters list is
empty.\n - Restored bottom borders for accordion items.\n - Standardized
vertical spacing across all sections (Date, Location, Format, Platform,
Search, and Sort) by removing redundant margins and padding for a clean,
contiguous look.

---------

Co-authored-by: Francesco Esposito <95753785+Francespo@users.noreply.github.com>
Franc3s-bot and others added 20 commits June 24, 2026 15:34
34 pyright errors fixed via type-ignore comments and one bug fix.
No SQL aggregation, list table schema, cache, or prewarm behavior
changed.

- Add 'from datetime import date' import (was missing)
- Add # pyright: ignore[reportIndexIssue,reportOptionalSubscript]
  for Tournament.location[...] JSONB subscript column expressions
  (the static type is Location | None but runtime is JSONB)
- Add # pyright: ignore[reportAttributeAccessIssue] for
  row.continent/country/city on Row[Any,Any,Any]
- Add # pyright: ignore[reportAttributeAccessIssue,reportOptionalMemberAccess]
  for Tournament.name.ilike, Tournament.format.in_, Tournament.source.in_
  (InstrumentedAttribute hidden by Pydantic field declarations)
- Add # pyright: ignore[reportAttributeAccessIssue] for sort_attr.asc/desc
  and various int|None -> int arg-type sites

Bug fix: date_start / date_end query params are now parsed via
date.fromisoformat() before comparison with Tournament.date (date).
On invalid input the filter is skipped silently rather than raising
a 500. The plan explicitly authorized this trade-off.
17 pyright errors fixed via imports, narrowing, and two real bug
fixes. No SQL aggregation, list table schema, cache, or prewarm
behavior changed.

- Add imports: from typing import Any, cast; Source; BaseScraper
- _delete_existing_tournament: change 'if existing:' to
  'if existing is not None:', capture eid = existing.id with assert,
  wrap column refs in cast(Any, ...) so pyright picks SQLAlchemy's
  ColumnOperators overload (returns ColumnElement[bool]) for
  delete(...).where(...)
- save_tournament_data: assign m.get('p1_name_temp') / 'p2_name_temp'
  to locals and isinstance(..., str)-guard before .strip()
- save_tournament_data: guard team_id_map[...] = ts.id with
  'if ts.id is not None:' (always set in practice; pyright doesn't
  know)
- main(): add narrowing guard 'if date_from is None or date_to is
  None: return 1' so executor.submit(scrape_platform, ...) and the
  direct call get non-Optional dates
- build_scrapers / _split_scrapers: tighten return type to
  list[tuple[str, BaseScraper]] (was object), so main()'s loop sees
  the concrete type and scraPer.list_tournaments() is a valid access

Two real bug fixes (beyond type-only changes):
1. Match(...) constructor was missing tournament_id=tournament.id
   in the dict-based path. The object-based path set it
   post-construction; the dict path was silently writing NULL FKs.
2. _write_sqlite: PlayerStanding(...) was also missing
   tournament_id=tournament.id for the same reason. The local
   variable tournament_id must be set on every persisted
   PlayerStanding row.
4 real implicit-any errors fixed via JSDoc. No build or runtime
behavior changed. Remaining diagnostics on this file are
environmental (no node_modules / @types/node in this worktree) and
go away when npm install is run in the user's environment.

- logConfig: @param {string} label, @param {unknown} value
- envSummary: @type {Record<string, string | undefined>}
- host: inline @type {string} on the .map((host) => ...) parameter
  in resolveAllowedHosts
Initialize `.slim/codemap.json` baseline (78 source files) and add
hierarchical `codemap.md` documents:

- Root atlas: project responsibility, entry points, directory map,
  cross-cutting data flow, integration points.
- backend/: FastAPI app, engine, SQLModel ORM, retried DB-init.
- backend/routers/: thin APIRouter layer (note: bulk of routes now in
  backend/api/).
- backend/api/: 10 versioned routers, Pydantic schemas, transactional
  detail reads, Ko-fi webhook, list-enrichment seam.
- backend/scrapers/: ListFortress/Longshanks/Rollbetter/YASB ingest.
- backend/data/: geocoding_cache.json write-through store.
- backend/data_structures/: canonical enums and Location value object.
- backend/analytics/: SQL-first filtering + Python aggregation.
- backend/utils/: geocoding, dedup, XWS canonicalization, YASB URLs.
- backend/utils/xwing_data/: cached wrapper over xwing-data2 JSON.
- backend/scripts/: operational scripts (scraper, dedup, migrations).
- frontend/: SvelteKit 2 + Svelte 5 runes app, stores, API client,
  server proxy, routes.

Register `AGENTS.md` with the standard Repository Map section so
OpenCode auto-discovers the codemap on session start.
Resolve pre-existing LSP/static-analysis warnings across 4 files:
- backend/api/tournaments.py (34 errors)
- backend/scripts/scrape_tournaments.py (17 errors)
- backend/api/formatters.py + backend/api/schemas.py (10 errors)
- frontend/vite.config.js (3 implicit-any errors)

Includes two real bug fixes surfaced during type cleanup:
- Match(...) and PlayerStanding(...) constructors in scrape_tournaments.py
  were missing tournament_id, silently writing NULL FKs in the dict-based
  save path and the _write_sqlite artifact path.
Replaces PR Francespo#92's partial fix with a from-scratch mobile shell, a
centralized filter URL-sync layer, and a stack of targeted perf and
responsiveness fixes. The 5 filterable routes no longer maintain
their own URL-building logic; the store owns it.

Mobile shell (new components):
- MobileTopBar: sticky top chrome for <md viewports (hamburger + brand)
- MobileNavDrawer: left-edge nav drawer, focus-trapped, ARIA dialog,
  sourced from Sidebar.NAV_LINKS module export
- MobileFilterDrawer: right-edge filter sheet accepting a children +
  optional footer snippet
- MobileFilterTrigger: FAB with active-filter count badge, <lg only
- DebouncedTextInput: 250ms-debounced search input

Filter system (filters.svelte.ts):
- toSearchParams(routeId): per-route whitelist serializer with
  deterministic key order (round-trip identity for the echo guard)
- applyFromSearchParams(params): URL -> store, partial-update only
- selectedFactions / sortBy / sortDirection: moved from route-local
  state into the store
- activeChips:  (was a getter that re-ran per access)

URL sync (lib/sync/urlSync.svelte.ts):
- scheduleSync(delayMs?, overlay?): debounced goto() with echo guard,
  URL-key round-trip identity, replaceState + keepFocus + noScroll
- clearPendingSync(): called by onNavigate in +layout.svelte so
  fast route switches do not corrupt the destination URL

Route rewrites (cards, lists, ships, squadrons, tournaments):
- Per-field URL-building  (20-64 lines each) replaced with
  5-10 line thin effect using filters.toSearchParams + scheduleSync
- route-local selectedFactions / sortBy / sortDirection removed
- echo-guard logic lives in urlSync, not in each route
- ~200 lines of duplicated URL plumbing deleted across 5 files

Per-route perf + responsive fixes:
- ships: 5 inline style attributes per card collapsed to 1 (with
  CSS custom properties); font-size: 10rem -> clamp(3rem, 18vw, 8rem)
- squadron/[signature]: 6-col pilot table collapses to stacked
  card layout on <sm
- ship/[xws]: right-edge gradient fade hints at horizontal scroll
  on <sm
- cards: dynamic import('$app/navigation') -> static goto import
  in tab handlers
- +page.svelte (dashboard): chartAction uses chart.update(newConfig)
  instead of destroy/recreate; chart.js auto-import hoisted to module
- TournamentFilters: $derived(() => fn()) misuse -> $derived.by

Misc:
- Sidebar: NAV_LINKS exported as the single source of truth for
  nav entries (consumed by both desktop sidebar and mobile drawer)
- FilterPanel: rewritten to desktop-only with a children snippet;
  routes pass the same snippet to MobileFilterDrawer for mobile
- MobileFilterDrawer / MobileNavDrawer: migrated from deprecated
  $app/stores to $app/state
- +layout.svelte: client-only $effect calls
  filters.applyFromSearchParams on mount, onNavigate cancels
  pending syncs

Touch targets: hamburger, nav links, FAB, sort direction button,
content-source toggle, drawer close buttons all >= 44x44. Active
filter count visible on the FAB badge and the drawer header.

Behavior: empty filters.sortBy / sortDirection produce empty URL
params which the backend fills with its own Query() defaults
(cards/ships/squadrons/lists: 'Popularity' or 'Games'; tournaments:
'Date'), so first-visit sort order is preserved.
The backend's startup cache prewarm was racing with db-seed's
pg_restore. The backend depends_on db-seed used condition:
service_started, which fires as soon as the db-seed container
starts (before the dump is actually loaded). The prewarm then
computed analytics against an empty database and cached empty
results, which the cache never invalidated (scrape_meta is
missing from the dump, so the version-based invalidation never
fires).

Add a healthcheck to db-seed that polls the `list` table for a
non-zero row count, and change the backend to depend on
service_healthy. This ensures the dump is fully restored before
the backend (and its prewarm) starts, so the cache is seeded
with real data on the first run.
Allows choosing the Vite dev server port on startup:
  bash scripts/local_dev/up.sh --port 4444
  bash scripts/local_dev/up.sh --port=4444

Default is unchanged (3333). Validates the port is a number
between 1 and 65535 and rejects unknown arguments. Also adds
--help / -h.

The four hardcoded 3333 references (ORIGIN, vite --port,
healthcheck URL, log-parser fallback) now use $VITE_PORT /
$DEFAULT_PORT.
- Add min-w-0 to main and card to prevent flex overflow on mobile
- Split layout into mobile (2-row) and desktop (3-column) at sm breakpoint
- Mobile: title + format chip + player count inline, date + location below
- Desktop: format badge column, info column, player count column
- Format label/color hoisted to @const per iteration for clarity
- Ensure 44px min tap target, proper truncation, no horizontal scroll
…tion, stats fix, upgrades

Round 13–22 frontend improvements:

Browser pages (tournaments, squadrons, lists, cards, ships):
- Remove DATE label from tournament rows; wrap location in styled pill
- Replace boxed 'N x FOUND' pills with faded-text below title (no box)
- Remove ActiveChips from main content; active filters only in sidebar
- Fix squadron count to show backend total (8663) instead of page capacity
- Apply custom Toggle to all checkboxes (squadrons, lists, cards, ships)
- Add SortBy component to every list section across all browser pages

Filter system:
- Create Toggle component (squared, 2px-radius, black bg, white check)
- Fix multi-select format bug (isFormatsAtDefault URL round-trip)
- Fix urlSync echo loop and stale-URL race condition
- Fix overlay-merge loop in urlSync (params.set multi-value bug)
- Add Format count badge to TournamentFilters label
- Add overflow-x-hidden to FilterPanel, MobileFilterDrawer, layout

Ship detail page:
- Add FactionIcon component (plain '?' for unknown factions, XWing glyph for known)
- Replace 21 callsites with FactionIcon across 14 files
- Fix match score colors (winner green pill, loser red, tie grey)
- Fix Gauntlet 5-color gradient (start%/end% hard-stop syntax)
- Fix ships page filter to honor selectedChips (frontend bug)
- Remove all glows from ship detail hero card

Sort and layout:
- Remove SortSelector from filter panels (replaced by inline SortBy)
- Add SortBy to dashboard sections, pilot detail, upgrade detail, etc.
- Fix SortBy labels: 'Popularity' → 'Games'/'Lists'
- Remove rounded rectangle wrapper from dashboard SortBy
- Fix Cards page SortBy to match filter dropdown options
- Remove 'Compatibility' option from pilot detail Top Configurations

Back navigation:
- BackLink uses history.back() with href fallback for fresh tabs
- All 6 detail pages benefit automatically (useHistory=true default)

Stats and data:
- Backend: fix ship_list_filter_clause with mode=all for AND semantics
- Backend: add list_id to PlayerStandingData schema
- Backend: add win_rate to aggregate_list_stats output
- Backend: clamp swiss_wins/losses/rank default from -1 to 0
- Data migration script for clamping negative stats
- Fix squadron win rate (was always 0% — use backend win_rate field)

Miscellaneous:
- Fix multi-faction ship gradient on ships browser page
- Fix content-source filter on pilot detail page
- Mobile: add Epic toggle to top bar, TournamentFilters to drawer
- Desktop sidebar: XWA/LEGACY/Epic compact toggle
- Add info tooltip to Tournament Filters label
- Fix horizontal scrollbar in filter column
- Remove all glows from ship detail hero card
All conflicts resolved in favor of dev (HEAD) which has the superior
architecture: normalized list table, SQL GROUP BY queries, filter_helpers,
ScrapeMeta model, BaseScraper type hints, coerce_list_json, pool tuning,
and the full mobile-first UI rebuild.

Upstream changes (minor: duplicate imports, pyright annotations already
present in dev) are subsets of what dev already contains.

Conflict files:
- .devcontainer/ (2): node feature versions, gemini-cli vs sshd
- .gitignore: slim/deepwork + playwright paths vs duplicate node_modules
- backend/analytics/ (5): SQL GROUP BY vs Python-loop aggregation
- backend/api/ (4): normalized list table queries vs legacy Python loops
- backend/infra (3): ScrapeMeta model, pool_size, coerce_list_json import
- backend/scripts/ (2): list table persistence, BaseScraper types
- frontend/+page.svelte: meta-snapshot URL builder (HEAD's params approach)
Co-authored-by: GitHub Copilot <copilot@users.noreply.github.com>
Co-authored-by: Frances-bot <263591795+Franc3s-bot@users.noreply.github.com>
58,603 playerstanding rows had swiss_wins=-1, swiss_losses=-1 as
scraper sentinels (rollbetter). COALESCE(..., 0) does not help since
-1 is not NULL, causing negative game counts and broken winrates.

Wrap all COALESCE in SUM() with GREATEST(0, ...) across:
- core.py (pilots + upgrades queries)
- ships.py
- squadrons.py
- lists.py
- factions.py (Python-side max(0, ...))

Database rows also cleaned to 0.
Preview deployments were failing because the SSR API routes used the
public backend URL (e.g. https://127.api.dev.m3tacron.com/api) which:
1. Requires DNS wildcard records that may not match Coolify's generated
   domain pattern (127.dev.api.m3tacron.com vs 127.api.dev.m3tacron.com)
2. Causes HTTP->HTTPS redirect loops when url.protocol is http: behind
   a reverse proxy (Traefik)

Now preview deployments talk directly to the backend container via the
internal Docker Compose network (http://backend:8888/api), which is
faster, simpler, and works regardless of external DNS configuration.
SvelteKit's url.hostname is derived from the ORIGIN env var, not the
actual request Host header. When ORIGIN=https://dev.m3tacron.com is set
for preview deployments, url.hostname resolves to dev.m3tacron.com
instead of 127.dev.m3tacron.com, causing the host-based preview URL
matching to fail.

Now we check ENV_VAR_SOURCE=preview or COOLIFY_BRANCH.startsWith('pull/')
to detect preview deployments and route API calls to the internal
Docker network (http://backend:8888/api) before host matching.
@m3tacron-dev-prod

m3tacron-dev-prod Bot commented Jul 31, 2026

Copy link
Copy Markdown

The preview deployment for francespo/m3tacron:main is ready. 🟢

Open frontend | Open backend | Open Build Logs | Open Application Logs

Last updated at: 2026-08-01 09:24:40 CET

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants