diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx
index 2973f93..fdfc631 100644
--- a/client/src/pages/DashboardPage.tsx
+++ b/client/src/pages/DashboardPage.tsx
@@ -32,6 +32,13 @@ import {
} from "../hooks/useSleeper";
import { usePowerRankings } from "../hooks/usePowerRankings";
import { getFamilySeasons } from "../utils/leagueFamily";
+import { getProviderLabel } from "../utils/providerLabels";
+import {
+ isSuperflex as isSuperflexLeague,
+ isTightEndPremium,
+ getScoringType,
+ getLeagueFormat,
+} from "../utils/sleeperNormalize";
import { useMyClaimedTeam } from "../hooks/useMyClaimedTeam";
import { useMyHuddles } from "../hooks/useHuddles";
import { Ticker } from "../widgets/dashboard/Ticker";
@@ -128,6 +135,18 @@ export function DashboardPage() {
? (leagueSettings["last_scored_leg"] as number)
: null;
+ // Masthead subheading — e.g. "Sleeper 12-team PPR TEP Superflex Dynasty League".
+ const mastheadSubheading = [
+ getProviderLabel(selectedLeague?.ref.provider),
+ leagueSettings["num_teams"] ? `${leagueSettings["num_teams"]}-team` : null,
+ getScoringType(selectedLeague?.scoringSettings ?? {}),
+ isTightEndPremium(selectedLeague?.scoringSettings ?? {}) ? "TEP" : null,
+ isSuperflexLeague(selectedLeague?.rosterPositions ?? []) ? "Superflex" : null,
+ `${getLeagueFormat(leagueSettings)} League`,
+ ]
+ .filter(Boolean)
+ .join(" ");
+
// Sleeper omits last_scored_leg for pre-draft / drafting leagues, so falling
// back to playoff_week_start would (incorrectly) advertise weeks 17/18 as
// navigable. Pin to 0 when the league hasn't kicked off so Scoreboard locks
@@ -251,6 +270,7 @@ export function DashboardPage() {
leagueName={selectedLeague?.name ?? ""}
week={week}
oldestYear={oldestYear}
+ subheading={mastheadSubheading}
/>
diff --git a/client/src/utils/providerLabels.ts b/client/src/utils/providerLabels.ts
new file mode 100644
index 0000000..9d9a918
--- /dev/null
+++ b/client/src/utils/providerLabels.ts
@@ -0,0 +1,13 @@
+import type { ProviderId } from "../types/fantasy";
+
+// Display copy per provider — not always a naive capitalize (e.g. ESPN is an
+// acronym, not a titlecased word).
+const PROVIDER_LABELS: Record
= {
+ sleeper: "Sleeper",
+ espn: "ESPN",
+ yahoo: "Yahoo",
+};
+
+export function getProviderLabel(provider: ProviderId | undefined): string {
+ return provider ? PROVIDER_LABELS[provider] : "";
+}
diff --git a/client/src/utils/sleeperNormalize.ts b/client/src/utils/sleeperNormalize.ts
index 78087b8..e1df855 100644
--- a/client/src/utils/sleeperNormalize.ts
+++ b/client/src/utils/sleeperNormalize.ts
@@ -177,3 +177,54 @@ export function buildDefStatsKeyMap(
return map;
}
+
+// ── League format ────────────────────────────────────────────────────────────
+
+/**
+ * A league is superflex when its roster_positions include a SUPER_FLEX slot
+ * (a flex spot that can also start a QB), as opposed to a plain FLEX slot.
+ *
+ * @param rosterPositions - League's `rosterPositions` field.
+ */
+export function isSuperflex(rosterPositions: string[]): boolean {
+ return rosterPositions.includes("SUPER_FLEX");
+}
+
+/**
+ * Tight end premium (TEP) leagues award tight ends extra points per
+ * reception on top of the standard `rec` value, via Sleeper's
+ * `bonus_rec_te` scoring key.
+ *
+ * @param scoringSettings - League's `scoringSettings` field.
+ */
+export function isTightEndPremium(scoringSettings: Record): boolean {
+ return (scoringSettings["bonus_rec_te"] ?? 0) > 0;
+}
+
+/**
+ * Derive the league's reception scoring format from the `rec` key in
+ * scoring_settings: 1 point/reception is full PPR, 0.5 is half-PPR, and
+ * 0 (or unset) is standard.
+ *
+ * @param scoringSettings - League's `scoringSettings` field.
+ */
+export function getScoringType(scoringSettings: Record): string {
+ const rec = scoringSettings["rec"] ?? 0;
+ if (rec >= 1) return "PPR";
+ if (rec > 0) return "Half-PPR";
+ return "Standard";
+}
+
+/**
+ * League format comes from Sleeper's `settings.type` field:
+ * 0 = redraft, 1 = keeper, 2 = dynasty.
+ *
+ * @param settings - League's `settings` field.
+ */
+export function getLeagueFormat(settings: Record): string {
+ const type = Number(settings["type"] ?? 0);
+ if (type === 3) return "Chopped";
+ if (type === 2) return "Dynasty";
+ if (type === 1) return "Keeper";
+ return "Redraft";
+}
diff --git a/client/src/widgets/dashboard/Masthead.tsx b/client/src/widgets/dashboard/Masthead.tsx
index 9c6a89e..5be38dd 100644
--- a/client/src/widgets/dashboard/Masthead.tsx
+++ b/client/src/widgets/dashboard/Masthead.tsx
@@ -10,15 +10,14 @@
* Wiring: `oldestYear` and `week` come from `DashboardPage`'s display-week
* derivation; `leagueName` is the selected league's display name.
*/
-export function Masthead({
- leagueName,
- week,
- oldestYear,
-}: {
+type MastheadProps = {
leagueName: string;
week: number;
oldestYear: string | null;
-}) {
+ subheading?: string;
+};
+
+export function Masthead({ leagueName, week, oldestYear, subheading }: MastheadProps) {
const now = new Date();
const dateStr = now.toLocaleDateString("en-US", {
weekday: "long",
@@ -39,7 +38,7 @@ export function Masthead({
{leagueName}
- [PLATFORM] [# OF TEAMS] [FORMAT]
+ {subheading}
diff --git a/server/src/providers/sleeper/index.ts b/server/src/providers/sleeper/index.ts
index 3c5695f..385cd0a 100644
--- a/server/src/providers/sleeper/index.ts
+++ b/server/src/providers/sleeper/index.ts
@@ -44,6 +44,16 @@ import type {
SleeperDraftPick,
} from "../../services/sleeperService.js";
+// Sleeper's `settings.type`: 0 = redraft, 1 = keeper, 2 = dynasty, 3 = chopped.
+// Chopped leagues play by different elimination rules we don't support yet,
+// so they're filtered out of league lookup at the import boundary rather
+// than handled (or half-handled) throughout the rest of the app.
+const CHOPPED_LEAGUE_TYPE = 3;
+
+function isChoppedLeague(s: SleeperLeague): boolean {
+ return Number(s.settings?.type) === CHOPPED_LEAGUE_TYPE;
+}
+
function toLeague(s: SleeperLeague): League {
return {
ref: { provider: "sleeper", leagueId: s.league_id },
@@ -253,7 +263,7 @@ export const sleeperProvider: FantasyProvider = {
async getUserLeagues(userId: string, year: string): Promise {
const leagues = await getSleeperLeagues(userId, year);
- return leagues.map(toLeague);
+ return leagues.filter((l) => !isChoppedLeague(l)).map(toLeague);
},
async getAllUserLeagues(userId: string): Promise {
@@ -273,6 +283,7 @@ export const sleeperProvider: FantasyProvider = {
seen.add(l.league_id);
return true;
})
+ .filter((l) => !isChoppedLeague(l))
.sort(
(a, b) =>
Number(b.season) - Number(a.season) || a.name.localeCompare(b.name),