Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/deploy-workers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ jobs:
run: npm run type-check
working-directory: workers/${{ matrix.worker }}

- name: Validate auth-worker Wrangler bindings
if: matrix.worker == 'auth-worker'
working-directory: workers/auth-worker
run: |
output="$(npx wrangler deploy --dry-run --env dev 2>&1)"
printf '%s\n' "$output"
if printf '%s\n' "$output" | grep -q 'Unexpected fields'; then
echo "Wrangler config contains unexpected fields"
exit 1
fi
printf '%s\n' "$output" | grep -q 'env.TOKEN_RATE_LIMITER'
printf '%s\n' "$output" | grep -q 'env.CREDENTIALS_RATE_LIMITER'

deploy:
name: Deploy ${{ matrix.worker }}
needs: test
Expand Down
2 changes: 1 addition & 1 deletion web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
# Run 'npm run dev' from the project root to start all services
NEXT_PUBLIC_AUTH_WORKER_URL=http://localhost:8786
NEXT_PUBLIC_FANTASY_MCP_URL=http://localhost:8790
NEXT_PUBLIC_ESPN_CLIENT_URL=http://localhost:8789
NEXT_PUBLIC_YAHOO_CLIENT_URL=http://localhost:8791
INTERNAL_SERVICE_TOKEN=replace-with-shared-internal-token

# ===========================================
# Environment Configuration
Expand Down
4 changes: 2 additions & 2 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ CLERK_SECRET_KEY=sk_...
# Worker URLs (unified gateway is primary)
NEXT_PUBLIC_AUTH_WORKER_URL=https://api.flaim.app/auth
NEXT_PUBLIC_FANTASY_MCP_URL=https://api.flaim.app/mcp
# ESPN onboarding (auto-pull + discover seasons)
NEXT_PUBLIC_ESPN_CLIENT_URL=https://espn-client.gerrygugger.workers.dev
# Shared internal token for server-to-worker helper calls
INTERNAL_SERVICE_TOKEN=...
# Yahoo MCP client (not called by web directly; useful for local debugging)
NEXT_PUBLIC_YAHOO_CLIENT_URL=https://yahoo-client.gerrygugger.workers.dev
```
Expand Down
213 changes: 11 additions & 202 deletions web/app/api/espn/auto-pull/route.ts
Original file line number Diff line number Diff line change
@@ -1,233 +1,42 @@
/**
* ESPN Auto-Pull API Route
* ---------------------------------------------------------------------------
* Simplified proxy to sport workers' /onboarding/initialize endpoints.
* Credentials and league management now handled by auth-worker.
*/

import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import type {
AutoPullResponse,
EspnLeagueInfo,
SportName
} from '@/lib/espn-types';
import { runEspnAutoPull } from '@/lib/server/espn-onboarding';

export async function POST(request: NextRequest) {
try {
const { userId, getToken } = await auth();

if (!userId) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}

const body = await request.json();
const { sport, leagueId, seasonYear: requestedSeasonYear } = body as {
const { sport, leagueId, seasonYear } = body as {
sport?: string;
leagueId?: string;
seasonYear?: number;
};

if (!sport || !leagueId) {
return NextResponse.json({
error: 'Missing required fields: sport and leagueId'
}, { status: 400 });
}

// Pass through user's requested year; ESPN-client computes default if omitted.
const seasonYear = requestedSeasonYear;

console.log(`[auto-pull] Using season year: ${seasonYear} (requested: ${requestedSeasonYear || 'none'})`);

const bearer = await getToken?.();
if (!bearer) {
console.error('[auto-pull] getToken() returned undefined');
return NextResponse.json({
error: 'Authentication token unavailable. Please try signing out and back in.',
code: 'TOKEN_UNAVAILABLE'
code: 'TOKEN_UNAVAILABLE',
}, { status: 401 });
}

// Fail fast if the user hasn't saved credentials in auth-worker yet.
const authWorkerUrl = process.env.NEXT_PUBLIC_AUTH_WORKER_URL;
if (!authWorkerUrl) {
return NextResponse.json({
error: 'NEXT_PUBLIC_AUTH_WORKER_URL is not configured'
}, { status: 500 });
}

console.log(`[auto-pull] Auth worker URL: ${authWorkerUrl}`);
console.log(`[auto-pull] Credential pre-check URL: ${authWorkerUrl}/credentials/espn`);

const credentialCheck = await fetch(`${authWorkerUrl}/credentials/espn`, {
headers: {
...(bearer ? { 'Authorization': `Bearer ${bearer}` } : {})
},
cache: 'no-store'
const result = await runEspnAutoPull({
sport,
leagueId,
seasonYear,
authHeader: `Bearer ${bearer}`,
correlationId: request.headers.get('X-Correlation-ID') || undefined,
});

console.log(`[auto-pull] Credential pre-check response: ${credentialCheck.status}`);

if (credentialCheck.status === 404) {
console.error('[auto-pull] Credential check returned 404');
return NextResponse.json({
error: 'ESPN credentials not found (credential check). Please add your ESPN credentials first.',
code: 'CREDENTIALS_MISSING_CHECK'
}, { status: 404 });
}

if (!credentialCheck.ok) {
const err = await credentialCheck.json().catch(() => ({})) as { error?: string };
return NextResponse.json({
error: err.error || 'Failed to verify ESPN credentials'
}, { status: credentialCheck.status });
}

const espnClientUrl = process.env.NEXT_PUBLIC_ESPN_CLIENT_URL;
const workerUrl = espnClientUrl;
console.log(`[auto-pull] ESPN client URL: ${espnClientUrl || 'NOT SET'}`);
console.log(`[auto-pull] Sport worker URL for ${sport}: ${workerUrl || 'NOT SET'}`);

if (!workerUrl) {
return NextResponse.json({
error: 'NEXT_PUBLIC_ESPN_CLIENT_URL is not configured'
}, { status: 500 });
}

try {
// Call the ESPN onboarding initialize endpoint
const sportWorkerFullUrl = `${workerUrl}/onboarding/initialize`;
console.log(`[auto-pull] Calling sport worker: ${sportWorkerFullUrl}`);
console.log(`[auto-pull] Sport worker request - userId: ${userId}, sport: ${sport}, leagueId: ${leagueId}, seasonYear: ${seasonYear}`);
console.log(`[auto-pull] Sport worker auth header: Bearer ${bearer ? `[${bearer.length} chars]` : 'MISSING'}`);

const workerResponse = await fetch(sportWorkerFullUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(bearer ? { 'Authorization': `Bearer ${bearer}` } : {})
},
body: JSON.stringify({
sport: sport,
leagueId: leagueId,
seasonYear: seasonYear
})
});

console.log(`[auto-pull] Sport worker response status: ${workerResponse.status}`);

if (!workerResponse.ok) {
const errorData = await workerResponse.json().catch(() => ({})) as {
code?: string;
error?: string;
};
console.error('Sport worker error:', workerResponse.status, errorData);

if (workerResponse.status === 404) {
if (errorData.code === 'CREDENTIALS_MISSING') {
console.error('[auto-pull] Sport worker returned CREDENTIALS_MISSING');
return NextResponse.json({
error: 'ESPN credentials not found (sport worker auth failed). Please add your ESPN credentials first.',
code: 'CREDENTIALS_MISSING_WORKER'
}, { status: 404 });
}

if (errorData.code === 'LEAGUES_MISSING') {
return NextResponse.json({
error: `No ${sport} leagues found. Please add ${sport} leagues first.`,
code: 'LEAGUES_MISSING'
}, { status: 404 });
}

return NextResponse.json({
error: errorData.error || 'Required data not found'
}, { status: 404 });
}

return NextResponse.json({
error: errorData.error || 'Failed to retrieve league information'
}, { status: workerResponse.status });
}

const workerData = await workerResponse.json() as {
success?: boolean;
error?: string;
leagues?: Array<{
leagueId: string;
sport: string;
teamId?: string;
leagueName?: string;
seasonYear?: number;
gameId?: string;
standings?: any[];
teams?: any[];
success?: boolean;
error?: string;
}>;
};

if (!workerData.success) {
return NextResponse.json({
error: workerData.error || 'Failed to retrieve league information'
}, { status: 400 });
}

// Transform sport worker response into expected format for the frontend
// Sport worker now returns info for the specific requested league
const targetLeague = workerData.leagues?.[0];
if (!targetLeague) {
return NextResponse.json({
error: 'No league data returned from sport worker'
}, { status: 404 });
}

// Check if the league itself has an error (e.g., ESPN returned 404 for this season)
if (targetLeague.success === false && targetLeague.error) {
return NextResponse.json({
error: targetLeague.error
}, { status: 404 });
}

const leagueInfo: EspnLeagueInfo = {
leagueId: targetLeague.leagueId,
leagueName: targetLeague.leagueName || `${sport} League ${targetLeague.leagueId}`,
sport: sport as SportName,
seasonYear: targetLeague.seasonYear || new Date().getFullYear(),
gameId: targetLeague.gameId || (sport === 'baseball' ? 'flb' : 'ffl'),
standings: targetLeague.standings || [],
teams: targetLeague.teams || []
};

// Validate that we got meaningful data
if (!leagueInfo.teams || leagueInfo.teams.length === 0) {
// Provide helpful message about season-specific issues
const suggestedYear = leagueInfo.seasonYear === new Date().getFullYear()
? leagueInfo.seasonYear - 1
: leagueInfo.seasonYear;
return NextResponse.json({
error: `No teams found for ${sport} league ${leagueId} in season ${leagueInfo.seasonYear}. ` +
`This league may not exist for this season. Try season ${suggestedYear} instead.`
}, { status: 404 });
}

const response: AutoPullResponse = {
success: true,
leagueInfo
};

return NextResponse.json(response);

} catch (workerError) {
console.error('Sport worker integration error:', workerError);
return NextResponse.json({
error: 'Failed to connect to league data service'
}, { status: 502 });
}

return NextResponse.json(result.body, { status: result.status });
} catch (error) {
console.error('Auto-pull API error:', error);
return NextResponse.json(
{ error: 'Failed to auto-pull league information' },
{ error: 'Failed to auto-pull league information' },
{ status: 500 }
);
}
Expand Down
56 changes: 10 additions & 46 deletions web/app/api/espn/discover-seasons/route.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,38 @@
/**
* ESPN Discover Seasons API Route
* ---------------------------------------------------------------------------
* Proxies to sport workers' /onboarding/discover-seasons endpoints.
* Probes ESPN API for all historical seasons of a league and auto-saves them.
*/

import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { runEspnDiscoverSeasons } from '@/lib/server/espn-onboarding';

export async function POST(request: NextRequest) {
try {
const { userId, getToken } = await auth();

if (!userId) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}

const body = await request.json();
const { sport, leagueId } = body as { sport?: string; leagueId?: string };

if (!sport || !leagueId) {
return NextResponse.json({
error: 'Missing required fields: sport and leagueId'
}, { status: 400 });
}

if (sport !== 'baseball' && sport !== 'football') {
return NextResponse.json({
error: 'Sport must be baseball or football'
error: 'Sport must be baseball or football',
}, { status: 400 });
}

const bearer = await getToken?.();
if (!bearer) {
return NextResponse.json({
error: 'Authentication token unavailable'
error: 'Authentication token unavailable',
}, { status: 401 });
}

const workerUrl = process.env.NEXT_PUBLIC_ESPN_CLIENT_URL;

if (!workerUrl) {
return NextResponse.json({
error: 'NEXT_PUBLIC_ESPN_CLIENT_URL is not configured'
}, { status: 500 });
}

console.log(`[discover-seasons] Calling ${sport} worker: ${workerUrl}/onboarding/discover-seasons`);

try {
const response = await fetch(`${workerUrl}/onboarding/discover-seasons`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${bearer}`
},
body: JSON.stringify({ leagueId, sport })
});

const data = await response.json();
console.log(`[discover-seasons] Worker response status: ${response.status}`);

return NextResponse.json(data, { status: response.status });
} catch (workerError) {
console.error('Worker connection error:', workerError);
return NextResponse.json({
error: 'Failed to connect to league discovery service'
}, { status: 502 });
}
const result = await runEspnDiscoverSeasons({
sport,
leagueId,
authHeader: `Bearer ${bearer}`,
correlationId: request.headers.get('X-Correlation-ID') || undefined,
});

return NextResponse.json(result.body, { status: result.status });
} catch (error) {
console.error('Discover seasons API error:', error);
return NextResponse.json(
Expand Down
Loading
Loading