feat: add GET /api/v2/refractor/cards list endpoint #172

Closed
opened 2026-03-25 06:10:57 +00:00 by cal · 2 comments
Owner

Summary

The Discord bot's /refractor status command calls GET /api/v2/refractor/cards?team_id=X to fetch all refractor card states for a team. This endpoint does not exist — only the single-card GET /refractor/cards/{card_id} does. Without it, /refractor status returns "No refractor data found" for all users.

A partial version exists at GET /api/v2/teams/{team_id}/refractors (teams.py:1533) but it lacks the filters the bot needs and lives in the wrong router.

Endpoint Spec

Route: GET /api/v2/refractor/cards

Query Parameters:

Param Type Required Default Description
team_id int yes Filter to a single team
card_type str no None "batter", "sp", or "rp"
tier int no None Filter by current tier (0-4)
season int no None Filter to players with season stats in given season
progress str no None "close" = within 80% of next threshold
limit int no 10 Page size (1-100)
offset int no 0 Items to skip

Response: {"count": N, "items": [...]}

Each item includes: player_id, player_name, team_id, current_tier, current_value, fully_evolved, last_evaluated_at, next_threshold, progress_pct, track (nested object with thresholds).

Sorting: current_tier DESC, current_value DESC

Key design decisions

  • player_name included in response to eliminate N+1 lookups in the bot
  • progress_pct pre-computed server-side for consistency
  • season filter uses EXISTS subquery against batting/pitching_season_stats
  • progress=close uses CASE expression mapping tier to appropriate threshold column
  • Empty results return 200 with count: 0, not 404
  • Recommend adding a (team_id) index on refractor_card_state

Full spec

See database/.claude/specs/refractor-cards-list-endpoint.md

Blocker for

  • Discord bot /refractor status command (all filters, pagination)
  • Refractor integration test plan (REF-01 through REF-34)

Migration note

Once this ships, deprecate GET /api/v2/teams/{team_id}/refractors in favor of this endpoint.

## Summary The Discord bot's `/refractor status` command calls `GET /api/v2/refractor/cards?team_id=X` to fetch all refractor card states for a team. This endpoint does not exist — only the single-card `GET /refractor/cards/{card_id}` does. Without it, `/refractor status` returns "No refractor data found" for all users. A partial version exists at `GET /api/v2/teams/{team_id}/refractors` (teams.py:1533) but it lacks the filters the bot needs and lives in the wrong router. ## Endpoint Spec **Route**: `GET /api/v2/refractor/cards` **Query Parameters**: | Param | Type | Required | Default | Description | |---|---|---|---|---| | `team_id` | int | **yes** | — | Filter to a single team | | `card_type` | str | no | None | `"batter"`, `"sp"`, or `"rp"` | | `tier` | int | no | None | Filter by current tier (0-4) | | `season` | int | no | None | Filter to players with season stats in given season | | `progress` | str | no | None | `"close"` = within 80% of next threshold | | `limit` | int | no | 10 | Page size (1-100) | | `offset` | int | no | 0 | Items to skip | **Response**: `{"count": N, "items": [...]}` Each item includes: `player_id`, `player_name`, `team_id`, `current_tier`, `current_value`, `fully_evolved`, `last_evaluated_at`, `next_threshold`, `progress_pct`, `track` (nested object with thresholds). **Sorting**: `current_tier DESC, current_value DESC` ## Key design decisions - `player_name` included in response to eliminate N+1 lookups in the bot - `progress_pct` pre-computed server-side for consistency - `season` filter uses EXISTS subquery against batting/pitching_season_stats - `progress=close` uses CASE expression mapping tier to appropriate threshold column - Empty results return 200 with `count: 0`, not 404 - Recommend adding a `(team_id)` index on `refractor_card_state` ## Full spec See `database/.claude/specs/refractor-cards-list-endpoint.md` ## Blocker for - Discord bot `/refractor status` command (all filters, pagination) - Refractor integration test plan (REF-01 through REF-34) ## Migration note Once this ships, deprecate `GET /api/v2/teams/{team_id}/refractors` in favor of this endpoint.
Author
Owner
Full endpoint spec (click to expand)

Spec: GET /api/v2/refractor/cards — List Refractor Card States

Status: Draft
Date: 2026-03-25
Author: pd-database (design advisor)


Background

The Discord bot's /refractor status command needs to display a paginated list of all cards on a team with their refractor progress. Currently, a similar endpoint exists at GET /api/v2/teams/{team_id}/refractors (in teams.py line 1533), but it is nested under the teams router and lacks several filters the bot requires (season, progress proximity). This spec defines a canonical endpoint on the refractor router that supersedes the teams-nested version.

Existing State

What Where
Single-card state GET /api/v2/refractor/cards/{card_id} in routers_v2/refractor.py
Team list (limited) GET /api/v2/teams/{team_id}/refractors in routers_v2/teams.py:1533
Response builder _build_card_state_response() in routers_v2/refractor.py:24
RefractorCardState model db_engine.py:1227 — FKs to Player, Team, RefractorTrack
RefractorTrack model db_engine.py:1213 — name, card_type, formula, t1-t4 thresholds
Unique index (player, team) on refractor_card_state

Route

GET /api/v2/refractor/cards

Lives in routers_v2/refractor.py alongside the existing single-card endpoint.


Query Parameters

Parameter Type Required Default Validation Description
team_id int yes Must reference an existing Team Filter states to a single team
card_type str no None One of "batter", "sp", "rp" Filter by RefractorTrack.card_type
tier int no None 0 <= tier <= 4 Filter by RefractorCardState.current_tier
season int no None Positive integer Filter to players who have season stats in the given season (see Query Strategy)
progress str no None Literal "close" Return only cards within 80% of their next threshold (excludes fully evolved)
limit int no 10 1 <= limit <= 100 Page size
offset int no 0 >= 0 Number of items to skip

Design Decision: limit/offset vs page/per_page

The existing teams-nested endpoint uses page/per_page. This spec uses limit/offset for the new endpoint because:

  1. limit/offset is the more standard REST convention and what the Discord bot client library expects.
  2. limit/offset composes better with cursor-based pagination if we migrate later.
  3. The teams-nested endpoint should be deprecated in favor of this one (see Migration Plan below).

Response Shape

{
  "count": 42,
  "items": [
    {
      "player_id": 1234,
      "player_name": "Mike Trout",
      "team_id": 7,
      "current_tier": 2,
      "current_value": 185.0,
      "fully_evolved": false,
      "last_evaluated_at": "2026-03-20T14:30:00",
      "next_threshold": 250,
      "progress_pct": 74.0,
      "track": {
        "id": 3,
        "name": "Slugger",
        "card_type": "batter",
        "formula": "pa + tb * 2",
        "t1_threshold": 50,
        "t2_threshold": 120,
        "t3_threshold": 250,
        "t4_threshold": 400
      }
    }
  ]
}

Field Definitions

Field Type Notes
count int Total matching rows (before limit/offset), for pagination UI
items[].player_id int Player.player_id FK
items[].player_name str Player.p_namenew field, not present in existing response
items[].team_id int Team.id FK
items[].current_tier int 0-4; current refractor tier
items[].current_value float Current computed formula value
items[].fully_evolved bool True when tier == 4
items[].last_evaluated_at str or null ISO-8601 datetime
items[].next_threshold int or null Threshold to reach next tier; null when fully evolved
items[].progress_pct float or null (current_value / next_threshold) * 100, rounded to 1 decimal; null when fully evolved
items[].track object Full RefractorTrack row (same as existing single-card response)

Changes from _build_card_state_response()

Two fields are added to the existing response shape:

  1. player_name — the bot currently has to make N+1 calls to resolve player names for display. Including the name in the list response eliminates this.
  2. progress_pct — pre-computed percentage toward next threshold. The bot currently calculates this client-side; moving it server-side ensures consistency and simplifies the progress=close filter.

The existing single-card endpoint (GET /refractor/cards/{card_id}) should also be updated to include these two fields for consistency, but that is a separate, non-blocking change.


ORM Query Strategy

Base Query

RefractorCardState
  .select(RefractorCardState, RefractorTrack, Player.p_name)
  .join(RefractorTrack)
  .switch(RefractorCardState)
  .join(Player, on=(RefractorCardState.player == Player.player_id))
  .where(RefractorCardState.team == team_id)
  .order_by(
      RefractorCardState.current_tier.desc(),
      RefractorCardState.current_value.desc()
  )

The Player join is required for player_name. Using .switch() to return to RefractorCardState before joining Player (Peewee multi-join pattern).

Filter: card_type

.where(RefractorTrack.card_type == card_type)

Filter: tier

.where(RefractorCardState.current_tier == tier)

Filter: season

This is the most complex filter. RefractorCardState does not store a season — it tracks career-aggregate progress. The season filter means: "only return states for players who have stats in this season" (i.e., players who were active in the given season).

Approach — EXISTS subquery:

from BattingSeasonStats, PitchingSeasonStats

batter_exists = (BattingSeasonStats
    .select()
    .where(
        (BattingSeasonStats.player == RefractorCardState.player) &
        (BattingSeasonStats.team == RefractorCardState.team) &
        (BattingSeasonStats.season == season)
    ))

pitcher_exists = (PitchingSeasonStats
    .select()
    .where(
        (PitchingSeasonStats.player == RefractorCardState.player) &
        (PitchingSeasonStats.team == RefractorCardState.team) &
        (PitchingSeasonStats.season == season)
    ))

query = query.where(
    fn.EXISTS(batter_exists) | fn.EXISTS(pitcher_exists)
)

This avoids duplicating rows or needing DISTINCT. The EXISTS subqueries will use the existing composite indexes on (player, team, season) that both season_stats tables already have.

Filter: progress

When progress=close, exclude fully evolved cards and filter to rows where current_value / next_threshold >= 0.8. Since "next threshold" depends on the current tier, this requires a CASE expression:

next_threshold_expr = Case(
    RefractorCardState.current_tier,
    (
        (0, RefractorTrack.t1_threshold),
        (1, RefractorTrack.t2_threshold),
        (2, RefractorTrack.t3_threshold),
        (3, RefractorTrack.t4_threshold),
    ),
    None
)

query = query.where(
    (RefractorCardState.fully_evolved == False) &
    (RefractorCardState.current_value >= next_threshold_expr * 0.8)
)

Pagination

total = query.count()  # before limit/offset
items = query.offset(offset).limit(limit)

count is returned in the response for the bot to render page indicators.


Sorting

Default sort: current_tier DESC, current_value DESC.

This puts the most progressed cards first, which is the natural display order for a "refractor status" view. Fully evolved (tier 4) cards appear at the top, followed by cards closest to their next tier-up.


Authentication

Same pattern as all other v2 endpoints:

token: str = Depends(oauth2_scheme)
if not valid_token(token):
    raise HTTPException(status_code=401, detail="Unauthorized")

Error Responses

Status Condition Body
401 Invalid or missing API token {"detail": "Unauthorized"}
422 Validation failure (e.g., tier=5, limit=0, card_type=invalid) FastAPI automatic validation error
200 Valid team_id but no refractor states {"count": 0, "items": []} — this is NOT an error

Design Decision: No 404 for empty results

An empty result set for a valid team_id is normal (team exists but has no refractor-eligible cards yet). Returning 200 with count: 0 is consistent with how GET /api/v2/refractor/tracks handles no results.

An invalid team_id (team does not exist in the DB) also returns {"count": 0, "items": []} because the query simply finds no matching rows. We do not validate team existence separately — this avoids an extra query.


Edge Cases

Case Behavior
Team has no refractor states {"count": 0, "items": []}
No RefractorTrack rows seeded All states reference a track, so if tracks are missing, states cannot exist. Returns empty.
progress=close with all cards fully evolved Empty list (fully evolved cards are excluded from the "close" filter)
season filter with no matching stats Empty list — no error
card_type combined with tier Filters are ANDed; both must match
Player deleted but RefractorCardState still exists The Player join would fail. The implementation should use a LEFT JOIN on Player or handle DoesNotExist gracefully by returning player_name: null.

Index Considerations

The query filters on RefractorCardState.team as its primary discriminator. Current indexes:

  • Existing: Unique index on (player, team) — this helps for the team filter but is not optimally ordered (team is second in the composite).
  • Recommended: Add a non-unique index on (team) alone.
CREATE INDEX idx_refractor_card_state_team
    ON refractor_card_state (team_id);

Migration Plan: Deprecate GET /api/v2/teams/{team_id}/refractors

The existing endpoint at teams.py:1533 overlaps significantly. After the new endpoint ships:

  1. Phase 1 (this release): Ship GET /api/v2/refractor/cards. Update Discord bot to call it.
  2. Phase 2 (next release): Add deprecation log warning to the teams-nested endpoint.
  3. Phase 3 (release after): Remove the teams-nested endpoint.

Summary of Files to Change

File Change
app/routers_v2/refractor.py Add GET /cards list endpoint; extend _build_card_state_response() with player_name and progress_pct
app/db_engine.py Add (team_id) index on refractor_card_state
app/routers_v2/teams.py No change in phase 1; deprecation log in phase 2
Tests New test file for the list endpoint covering: basic fetch, each filter, pagination, empty results, progress filter math
<details> <summary>Full endpoint spec (click to expand)</summary> # Spec: `GET /api/v2/refractor/cards` — List Refractor Card States **Status**: Draft **Date**: 2026-03-25 **Author**: pd-database (design advisor) --- ## Background The Discord bot's `/refractor status` command needs to display a paginated list of all cards on a team with their refractor progress. Currently, a similar endpoint exists at `GET /api/v2/teams/{team_id}/refractors` (in `teams.py` line 1533), but it is nested under the teams router and lacks several filters the bot requires (season, progress proximity). This spec defines a canonical endpoint on the refractor router that supersedes the teams-nested version. ### Existing State | What | Where | |---|---| | Single-card state | `GET /api/v2/refractor/cards/{card_id}` in `routers_v2/refractor.py` | | Team list (limited) | `GET /api/v2/teams/{team_id}/refractors` in `routers_v2/teams.py:1533` | | Response builder | `_build_card_state_response()` in `routers_v2/refractor.py:24` | | RefractorCardState model | `db_engine.py:1227` — FKs to Player, Team, RefractorTrack | | RefractorTrack model | `db_engine.py:1213` — name, card_type, formula, t1-t4 thresholds | | Unique index | `(player, team)` on refractor_card_state | --- ## Route ``` GET /api/v2/refractor/cards ``` Lives in `routers_v2/refractor.py` alongside the existing single-card endpoint. --- ## Query Parameters | Parameter | Type | Required | Default | Validation | Description | |---|---|---|---|---|---| | `team_id` | `int` | **yes** | — | Must reference an existing Team | Filter states to a single team | | `card_type` | `str` | no | `None` | One of `"batter"`, `"sp"`, `"rp"` | Filter by `RefractorTrack.card_type` | | `tier` | `int` | no | `None` | `0 <= tier <= 4` | Filter by `RefractorCardState.current_tier` | | `season` | `int` | no | `None` | Positive integer | Filter to players who have season stats in the given season (see Query Strategy) | | `progress` | `str` | no | `None` | Literal `"close"` | Return only cards within 80% of their next threshold (excludes fully evolved) | | `limit` | `int` | no | `10` | `1 <= limit <= 100` | Page size | | `offset` | `int` | no | `0` | `>= 0` | Number of items to skip | ### Design Decision: `limit`/`offset` vs `page`/`per_page` The existing teams-nested endpoint uses `page`/`per_page`. This spec uses `limit`/`offset` for the new endpoint because: 1. `limit`/`offset` is the more standard REST convention and what the Discord bot client library expects. 2. `limit`/`offset` composes better with cursor-based pagination if we migrate later. 3. The teams-nested endpoint should be deprecated in favor of this one (see Migration Plan below). --- ## Response Shape ```json { "count": 42, "items": [ { "player_id": 1234, "player_name": "Mike Trout", "team_id": 7, "current_tier": 2, "current_value": 185.0, "fully_evolved": false, "last_evaluated_at": "2026-03-20T14:30:00", "next_threshold": 250, "progress_pct": 74.0, "track": { "id": 3, "name": "Slugger", "card_type": "batter", "formula": "pa + tb * 2", "t1_threshold": 50, "t2_threshold": 120, "t3_threshold": 250, "t4_threshold": 400 } } ] } ``` ### Field Definitions | Field | Type | Notes | |---|---|---| | `count` | `int` | Total matching rows (before limit/offset), for pagination UI | | `items[].player_id` | `int` | `Player.player_id` FK | | `items[].player_name` | `str` | `Player.p_name` — **new field**, not present in existing response | | `items[].team_id` | `int` | `Team.id` FK | | `items[].current_tier` | `int` | 0-4; current refractor tier | | `items[].current_value` | `float` | Current computed formula value | | `items[].fully_evolved` | `bool` | True when tier == 4 | | `items[].last_evaluated_at` | `str` or `null` | ISO-8601 datetime | | `items[].next_threshold` | `int` or `null` | Threshold to reach next tier; `null` when fully evolved | | `items[].progress_pct` | `float` or `null` | `(current_value / next_threshold) * 100`, rounded to 1 decimal; `null` when fully evolved | | `items[].track` | `object` | Full RefractorTrack row (same as existing single-card response) | ### Changes from `_build_card_state_response()` Two fields are added to the existing response shape: 1. **`player_name`** — the bot currently has to make N+1 calls to resolve player names for display. Including the name in the list response eliminates this. 2. **`progress_pct`** — pre-computed percentage toward next threshold. The bot currently calculates this client-side; moving it server-side ensures consistency and simplifies the `progress=close` filter. The existing single-card endpoint (`GET /refractor/cards/{card_id}`) should also be updated to include these two fields for consistency, but that is a separate, non-blocking change. --- ## ORM Query Strategy ### Base Query ``` RefractorCardState .select(RefractorCardState, RefractorTrack, Player.p_name) .join(RefractorTrack) .switch(RefractorCardState) .join(Player, on=(RefractorCardState.player == Player.player_id)) .where(RefractorCardState.team == team_id) .order_by( RefractorCardState.current_tier.desc(), RefractorCardState.current_value.desc() ) ``` The Player join is required for `player_name`. Using `.switch()` to return to RefractorCardState before joining Player (Peewee multi-join pattern). ### Filter: `card_type` ``` .where(RefractorTrack.card_type == card_type) ``` ### Filter: `tier` ``` .where(RefractorCardState.current_tier == tier) ``` ### Filter: `season` This is the most complex filter. `RefractorCardState` does not store a season — it tracks career-aggregate progress. The `season` filter means: "only return states for players who have stats in this season" (i.e., players who were active in the given season). Approach — EXISTS subquery: ``` from BattingSeasonStats, PitchingSeasonStats batter_exists = (BattingSeasonStats .select() .where( (BattingSeasonStats.player == RefractorCardState.player) & (BattingSeasonStats.team == RefractorCardState.team) & (BattingSeasonStats.season == season) )) pitcher_exists = (PitchingSeasonStats .select() .where( (PitchingSeasonStats.player == RefractorCardState.player) & (PitchingSeasonStats.team == RefractorCardState.team) & (PitchingSeasonStats.season == season) )) query = query.where( fn.EXISTS(batter_exists) | fn.EXISTS(pitcher_exists) ) ``` This avoids duplicating rows or needing DISTINCT. The EXISTS subqueries will use the existing composite indexes on `(player, team, season)` that both season_stats tables already have. ### Filter: `progress` When `progress=close`, exclude fully evolved cards and filter to rows where `current_value / next_threshold >= 0.8`. Since "next threshold" depends on the current tier, this requires a CASE expression: ``` next_threshold_expr = Case( RefractorCardState.current_tier, ( (0, RefractorTrack.t1_threshold), (1, RefractorTrack.t2_threshold), (2, RefractorTrack.t3_threshold), (3, RefractorTrack.t4_threshold), ), None ) query = query.where( (RefractorCardState.fully_evolved == False) & (RefractorCardState.current_value >= next_threshold_expr * 0.8) ) ``` ### Pagination ``` total = query.count() # before limit/offset items = query.offset(offset).limit(limit) ``` `count` is returned in the response for the bot to render page indicators. --- ## Sorting Default sort: `current_tier DESC, current_value DESC`. This puts the most progressed cards first, which is the natural display order for a "refractor status" view. Fully evolved (tier 4) cards appear at the top, followed by cards closest to their next tier-up. --- ## Authentication Same pattern as all other v2 endpoints: ```python token: str = Depends(oauth2_scheme) if not valid_token(token): raise HTTPException(status_code=401, detail="Unauthorized") ``` --- ## Error Responses | Status | Condition | Body | |---|---|---| | 401 | Invalid or missing API token | `{"detail": "Unauthorized"}` | | 422 | Validation failure (e.g., `tier=5`, `limit=0`, `card_type=invalid`) | FastAPI automatic validation error | | 200 | Valid team_id but no refractor states | `{"count": 0, "items": []}` — this is NOT an error | ### Design Decision: No 404 for empty results An empty result set for a valid `team_id` is normal (team exists but has no refractor-eligible cards yet). Returning 200 with `count: 0` is consistent with how `GET /api/v2/refractor/tracks` handles no results. An invalid `team_id` (team does not exist in the DB) also returns `{"count": 0, "items": []}` because the query simply finds no matching rows. We do not validate team existence separately — this avoids an extra query. --- ## Edge Cases | Case | Behavior | |---|---| | Team has no refractor states | `{"count": 0, "items": []}` | | No RefractorTrack rows seeded | All states reference a track, so if tracks are missing, states cannot exist. Returns empty. | | `progress=close` with all cards fully evolved | Empty list (fully evolved cards are excluded from the "close" filter) | | `season` filter with no matching stats | Empty list — no error | | `card_type` combined with `tier` | Filters are ANDed; both must match | | Player deleted but RefractorCardState still exists | The Player join would fail. The implementation should use a LEFT JOIN on Player or handle DoesNotExist gracefully by returning `player_name: null`. | --- ## Index Considerations The query filters on `RefractorCardState.team` as its primary discriminator. Current indexes: - **Existing**: Unique index on `(player, team)` — this helps for the team filter but is not optimally ordered (team is second in the composite). - **Recommended**: Add a non-unique index on `(team)` alone. ```sql CREATE INDEX idx_refractor_card_state_team ON refractor_card_state (team_id); ``` --- ## Migration Plan: Deprecate `GET /api/v2/teams/{team_id}/refractors` The existing endpoint at `teams.py:1533` overlaps significantly. After the new endpoint ships: 1. **Phase 1** (this release): Ship `GET /api/v2/refractor/cards`. Update Discord bot to call it. 2. **Phase 2** (next release): Add deprecation log warning to the teams-nested endpoint. 3. **Phase 3** (release after): Remove the teams-nested endpoint. --- ## Summary of Files to Change | File | Change | |---|---| | `app/routers_v2/refractor.py` | Add `GET /cards` list endpoint; extend `_build_card_state_response()` with `player_name` and `progress_pct` | | `app/db_engine.py` | Add `(team_id)` index on `refractor_card_state` | | `app/routers_v2/teams.py` | No change in phase 1; deprecation log in phase 2 | | Tests | New test file for the list endpoint covering: basic fetch, each filter, pagination, empty results, progress filter math | </details>
Claude added the
ai-working
label 2026-03-25 06:31:17 +00:00
Claude removed the
ai-working
label 2026-03-25 06:35:49 +00:00
Collaborator

PR #173 opens the implementation: #173

Approach:

  • New GET /api/v2/refractor/cards on the refractor router with all filters from the spec (team_id, card_type, tier, season, progress, limit, offset)
  • season filter uses EXISTS subqueries against batting_season_stats and pitching_season_stats to avoid duplicates from players with both types
  • progress=close uses a Peewee Case expression to map current_tier → next threshold column, then filters current_value >= next_threshold * 0.8
  • LEFT JOIN on Player so deleted players degrade gracefully to player_name: null
  • _build_card_state_response() extended with progress_pct (always computed) and optional player_name; single-card endpoint gains progress_pct as a non-breaking addition
  • Migration SQL included: 2026-03-25_add_refractor_card_state_team_index.sql
PR #173 opens the implementation: https://git.manticorum.com/cal/paper-dynasty-database/pulls/173 **Approach:** - New `GET /api/v2/refractor/cards` on the refractor router with all filters from the spec (`team_id`, `card_type`, `tier`, `season`, `progress`, `limit`, `offset`) - `season` filter uses EXISTS subqueries against `batting_season_stats` and `pitching_season_stats` to avoid duplicates from players with both types - `progress=close` uses a Peewee `Case` expression to map `current_tier` → next threshold column, then filters `current_value >= next_threshold * 0.8` - LEFT JOIN on Player so deleted players degrade gracefully to `player_name: null` - `_build_card_state_response()` extended with `progress_pct` (always computed) and optional `player_name`; single-card endpoint gains `progress_pct` as a non-breaking addition - Migration SQL included: `2026-03-25_add_refractor_card_state_team_index.sql`
Claude added the
ai-pr-opened
label 2026-03-25 06:36:00 +00:00
cal closed this issue 2026-03-25 14:52:25 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: cal/paper-dynasty-database#172
No description provided.