This commit is contained in:
Eduardo Figueroa 2025-09-04 21:31:23 -07:00
commit 0718fdd01f
13 changed files with 1197 additions and 0 deletions

18
ContainerFile Normal file
View file

@ -0,0 +1,18 @@
FROM python:3.11-slim
WORKDIR /app
# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user for security
RUN useradd --create-home --shell /bin/bash app && chown -R app:app /app
USER app
EXPOSE 5000
CMD ["python", "app.py"]

252
app.py Normal file
View file

@ -0,0 +1,252 @@
from flask import Flask, render_template, jsonify
from datetime import datetime
import os
import sys
import traceback
from services.sleeper_api import SleeperAPI
from services.espn_api import ESPNAPI
from config import Config
# Force unbuffered output for Docker logs
os.environ['PYTHONUNBUFFERED'] = '1'
print("=== FantasyCron Starting ===", flush=True)
print(f"=== Version: {Config.VERSION} ===", flush=True)
app = Flask(__name__)
app.config.from_object(Config)
# Initialize API services
sleeper_api = SleeperAPI()
espn_api = ESPNAPI()
def get_league_color(league_index):
"""Assign colors to leagues in order"""
colors = app.config['LEAGUE_COLORS']
return colors[league_index % len(colors)] # Cycle through colors
@app.context_processor
def inject_apis():
"""Make API and version available to all templates"""
return dict(sleeper_api=sleeper_api, app_version=app.config['VERSION'])
@app.route('/')
def index():
"""Home page with username input"""
print("DEBUG: Index route accessed", flush=True)
return render_template('index.html')
@app.route('/<username>')
def dashboard_current(username):
"""Dashboard for current NFL week"""
print(f"DEBUG: Dashboard current route - username: '{username}'", flush=True)
try:
nfl_state = sleeper_api.get_nfl_state()
current_week = nfl_state.get('display_week', 1)
print(f"DEBUG: Current week: {current_week}", flush=True)
return dashboard(username, current_week)
except Exception as e:
print(f"ERROR: dashboard_current exception - {str(e)}", flush=True)
print(f"ERROR: Full traceback: {traceback.format_exc()}", flush=True)
return render_template('error.html',
message=f"Could not find user '{username}'. Please check the username and try again.")
@app.route('/<username>/<int:week>')
def dashboard_week(username, week):
"""Dashboard for specific week"""
print(f"DEBUG: Dashboard week - username: '{username}', week: {week}", flush=True)
return dashboard(username, week)
def dashboard(username, week):
"""Main dashboard logic - fetch and display user's fantasy data"""
print(f"DEBUG: Dashboard function START - username: '{username}', week: {week}", flush=True)
try:
# Get user info from Sleeper
print(f"DEBUG: Calling get_user() with username: '{username}'", flush=True)
user = sleeper_api.get_user(username)
print(f"DEBUG: get_user() returned: {user}", flush=True)
if not user:
print(f"WARNING: User lookup failed for username: '{username}'", flush=True)
return render_template('error.html',
message=f"Could not find user '{username}'. Please check the username and try again.")
print(f"DEBUG: User found - ID: {user.get('user_id')}, Display: {user.get('display_name', 'Unknown')}", flush=True)
# Get current NFL season
print("DEBUG: Calling get_nfl_state()", flush=True)
nfl_state = sleeper_api.get_nfl_state()
season = nfl_state.get('season', str(datetime.now().year))
print(f"DEBUG: Using season: {season}", flush=True)
# Get user's fantasy leagues
user_id = user['user_id']
print(f"DEBUG: Calling get_user_leagues() with user_id: '{user_id}', season: '{season}'", flush=True)
leagues = sleeper_api.get_user_leagues(user_id, season)
print(f"DEBUG: Found {len(leagues) if leagues else 0} leagues", flush=True)
if leagues:
for i, league in enumerate(leagues):
print(f"DEBUG: League {i+1}: ID={league.get('league_id')}, Name='{league.get('name')}'", flush=True)
# Get NFL game schedule for the week
print(f"DEBUG: Calling get_week_schedule() for week {week}, season {season}", flush=True)
try:
schedule = espn_api.get_week_schedule(week, season)
print(f"DEBUG: Schedule retrieved successfully", flush=True)
except Exception as e:
print(f"ERROR: ESPN schedule fetch failed: {str(e)}", flush=True)
schedule = {}
# Process each league for matchup data
league_data = []
for i, league in enumerate(leagues):
print(f"DEBUG: ===== Processing league {i+1}/{len(leagues)}: '{league['name']}' =====", flush=True)
league_id = league['league_id']
try:
# Get matchups for the current week
print(f"DEBUG: Calling get_matchups() for league {league_id}, week {week}", flush=True)
matchups = sleeper_api.get_matchups(league_id, week)
print(f"DEBUG: Found {len(matchups) if matchups else 0} matchups", flush=True)
# Get rosters to find user's team
print(f"DEBUG: Calling get_rosters() for league {league_id}", flush=True)
rosters = sleeper_api.get_rosters(league_id)
print(f"DEBUG: Found {len(rosters) if rosters else 0} rosters", flush=True)
# Find user's roster in this league
user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None)
print(f"DEBUG: User roster found: {user_roster is not None}", flush=True)
if user_roster and matchups:
# Find user's matchup for this week
user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None)
print(f"DEBUG: User matchup found: {user_matchup is not None}", flush=True)
# Find opponent's matchup and user info
opponent_matchup = None
opponent_user = None
if user_matchup:
# Find opponent in same matchup
opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None)
print(f"DEBUG: Opponent matchup found: {opponent_matchup is not None}", flush=True)
if opponent_matchup:
# Get opponent's roster to find owner
opponent_roster = next((r for r in rosters if r['roster_id'] == opponent_matchup['roster_id']), None)
if opponent_roster:
opponent_owner_id = opponent_roster['owner_id']
# Fetch opponent's user profile
opponent_user = sleeper_api.get_user_by_id(opponent_owner_id)
print(f"DEBUG: Opponent user: {opponent_user.get('display_name') if opponent_user else 'None'}", flush=True)
# Get all players on user's roster for calendar
all_players = []
if user_roster and user_roster.get('players'):
players_list = user_roster['players']
print(f"DEBUG: Processing {len(players_list)} total players for calendar", flush=True)
for player_id in players_list:
try:
# Get player details from Sleeper API
player = sleeper_api.get_player_info(player_id)
if player:
all_players.append(player)
except Exception as e:
print(f"ERROR: Failed to get player info for {player_id}: {str(e)}", flush=True)
# Store processed league data
league_info = {
'league': league,
'league_color': get_league_color(i), # Assign color by order
'user_matchup': user_matchup,
'opponent_matchup': opponent_matchup,
'opponent_user': opponent_user,
'user_roster': user_roster,
'all_players': all_players
}
league_data.append(league_info)
print(f"DEBUG: League '{league['name']}' processed successfully", flush=True)
else:
print(f"DEBUG: Skipping league '{league['name']}' - missing data", flush=True)
except Exception as e:
print(f"ERROR: Failed to process league '{league['name']}': {str(e)}", flush=True)
print(f"ERROR: League processing traceback: {traceback.format_exc()}", flush=True)
# Debug output before rendering template
print(f"DEBUG: About to render template with {len(league_data)} leagues", flush=True)
for league_info in league_data:
print(f"DEBUG: League '{league_info['league']['name']}' has {len(league_info['all_players'])} players", flush=True)
total_game_count = sum(len(games) for games in schedule.values())
print(f"DEBUG: Schedule has {total_game_count} total games", flush=True)
try:
# Render dashboard with all collected data
result = render_template('dashboard.html',
user=user,
week=week,
league_data=league_data,
schedule=schedule,
nfl_state=nfl_state)
print(f"DEBUG: Template rendered successfully", flush=True)
return result
except Exception as e:
print(f"ERROR: Template rendering failed: {str(e)}", flush=True)
print(f"ERROR: Template rendering traceback: {traceback.format_exc()}", flush=True)
return render_template('error.html', message=f"Template error: {str(e)}")
except Exception as e:
print(f"ERROR: Dashboard function exception: {str(e)}", flush=True)
print(f"ERROR: Full dashboard traceback: {traceback.format_exc()}", flush=True)
return render_template('error.html',
message=f"Error loading data: {str(e)}")
@app.route('/api/refresh/<username>/<int:week>')
def refresh_data(username, week):
"""API endpoint for live score updates"""
print(f"DEBUG: Refresh data API - username: '{username}', week: {week}", flush=True)
try:
# Get user and current season
user = sleeper_api.get_user(username)
if not user:
return jsonify({'error': 'User not found'}), 404
nfl_state = sleeper_api.get_nfl_state()
season = nfl_state.get('season', str(datetime.now().year))
leagues = sleeper_api.get_user_leagues(user['user_id'], season)
league_scores = []
# Get updated scores for each league
for league in leagues:
matchups = sleeper_api.get_matchups(league['league_id'], week)
rosters = sleeper_api.get_rosters(league['league_id'])
user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None)
if user_roster and matchups:
# Find user and opponent matchups
user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None)
opponent_matchup = None
if user_matchup:
opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None)
# Return current points
league_scores.append({
'league_id': league['league_id'],
'league_name': league['name'],
'user_points': user_matchup['points'] if user_matchup else 0,
'opponent_points': opponent_matchup['points'] if opponent_matchup else 0
})
return jsonify({'league_scores': league_scores})
except Exception as e:
print(f"ERROR: Refresh data exception: {str(e)}", flush=True)
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
print("DEBUG: Starting Flask app on 0.0.0.0:5000", flush=True)
print(f"DEBUG: FantasyCron {app.config['VERSION']} ready!", flush=True)
app.run(host='0.0.0.0', port=5000, debug=True)

27
compose.yaml Normal file
View file

@ -0,0 +1,27 @@
services:
fantasycron:
image: fantasycron:latest # Use pre-built image
ports:
- "5000:5000"
environment:
- SECRET_KEY=your-secret-key-here
- DATABASE_URL=sqlite:///fantasy_app.db
volumes:
- ./data:/app/data
restart: unless-stopped
# Optional PostgreSQL service
# Uncomment to use PostgreSQL instead of SQLite
# postgres:
# image: postgres:13
# environment:
# POSTGRES_DB: fantasy_app
# POSTGRES_USER: fantasy_user
# POSTGRES_PASSWORD: fantasy_pass
# volumes:
# - postgres_data:/var/lib/postgresql/data
# ports:
# - "5432:5432"
# volumes:
# postgres_data:

20
config.py Normal file
View file

@ -0,0 +1,20 @@
import os
class Config:
# App version
VERSION = '0.1.0'
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
# Database settings - support SQLite, PostgreSQL, MariaDB
DATABASE_URL = os.environ.get('DATABASE_URL') or 'sqlite:///fantasy_app.db'
# API settings
SLEEPER_BASE_URL = 'https://api.sleeper.app/v1'
ESPN_BASE_URL = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl'
# League colors - assigned in order
LEAGUE_COLORS = [
'#006400', '#00FFFF', '#FF0000', '#FFD700', '#1E90FF',
'#C71585', '#00FF00', '#00FFFF', '#0000FF', '#1E90FF'
]

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
Flask==2.3.3
requests==2.31.0
python-dateutil==2.8.2

1
services/__init__.py Normal file
View file

@ -0,0 +1 @@
# Empty file to make services a package

92
services/espn_api.py Normal file
View file

@ -0,0 +1,92 @@
import requests
from datetime import datetime, timedelta
class ESPNAPI:
BASE_URL = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl'
def __init__(self):
self.session = requests.Session()
def get_week_schedule(self, week, season):
"""Get NFL schedule for a specific week"""
try:
print(f"ESPN API: Fetching schedule for week {week}, season {season}", flush=True)
# Get current NFL scoreboard
url = f"{self.BASE_URL}/scoreboard"
response = self.session.get(url)
response.raise_for_status()
data = response.json()
print(f"ESPN API: Response status: {response.status_code}", flush=True)
games = []
if 'events' in data:
print(f"ESPN API: Found {len(data['events'])} events", flush=True)
for i, event in enumerate(data['events']):
try:
# Parse the game date
game_date = datetime.strptime(event['date'], '%Y-%m-%dT%H:%MZ')
# Extract team information
competitors = event['competitions'][0]['competitors']
home_team = None
away_team = None
# Identify home and away teams
for comp in competitors:
team_abbrev = comp['team']['abbreviation']
if comp['homeAway'] == 'home':
home_team = team_abbrev
else:
away_team = team_abbrev
print(f"ESPN API: {away_team} @ {home_team} at {game_date}", flush=True)
# Store game data
games.append({
'date': game_date,
'day_of_week': game_date.strftime('%A'),
'time': game_date.strftime('%I:%M %p'),
'home_team': home_team,
'away_team': away_team,
'teams': [home_team, away_team]
})
except Exception as e:
print(f"ESPN API: Error processing event {i+1}: {str(e)}", flush=True)
else:
print("ESPN API: No 'events' key found in response", flush=True)
print(f"ESPN API: Processed {len(games)} games total", flush=True)
schedule = self._organize_by_day(games)
# Log games by day
for day, day_games in schedule.items():
print(f"ESPN API: {day}: {len(day_games)} games", flush=True)
return schedule
except Exception as e:
print(f"ESPN API: Error fetching schedule: {e}", flush=True)
return {}
def _organize_by_day(self, games):
"""Organize games by day of week - dynamically include all days with games"""
schedule = {}
# Group games by day
for game in games:
day = game['day_of_week']
if day not in schedule:
schedule[day] = []
schedule[day].append(game)
# Sort days by chronological order (Monday=0, Sunday=6)
day_order = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
sorted_schedule = {}
for day in sorted(schedule.keys(), key=lambda x: day_order.get(x, 7)):
sorted_schedule[day] = schedule[day]
return sorted_schedule

72
services/sleeper_api.py Normal file
View file

@ -0,0 +1,72 @@
import requests
from datetime import datetime
class SleeperAPI:
BASE_URL = 'https://api.sleeper.app/v1'
def __init__(self):
self.session = requests.Session()
self.players_cache = None # Cache player data
self.players_updated = None # Track last update time
def get_user(self, username):
"""Get user info by username"""
try:
response = self.session.get(f"{self.BASE_URL}/user/{username}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException:
return None
def get_user_by_id(self, user_id):
"""Get user info by user_id"""
try:
response = self.session.get(f"{self.BASE_URL}/user/{user_id}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException:
return None
def get_nfl_state(self):
"""Get current NFL state (week, season, etc)"""
response = self.session.get(f"{self.BASE_URL}/state/nfl")
response.raise_for_status()
return response.json()
def get_user_leagues(self, user_id, season):
"""Get all leagues for a user in a season"""
response = self.session.get(f"{self.BASE_URL}/user/{user_id}/leagues/nfl/{season}")
response.raise_for_status()
return response.json()
def get_matchups(self, league_id, week):
"""Get matchups for a league and week"""
response = self.session.get(f"{self.BASE_URL}/league/{league_id}/matchups/{week}")
response.raise_for_status()
return response.json()
def get_rosters(self, league_id):
"""Get rosters for a league"""
response = self.session.get(f"{self.BASE_URL}/league/{league_id}/rosters")
response.raise_for_status()
return response.json()
def get_players(self, force_refresh=False):
"""Get all NFL players (cached for 24 hours)"""
now = datetime.now()
# Check if cache is stale or force refresh requested
if (not self.players_cache or not self.players_updated or
(now - self.players_updated).total_seconds() > 86400 or force_refresh):
response = self.session.get(f"{self.BASE_URL}/players/nfl")
response.raise_for_status()
self.players_cache = response.json()
self.players_updated = now
return self.players_cache
def get_player_info(self, player_id):
"""Get specific player info"""
players = self.get_players()
return players.get(str(player_id))

509
static/style.css Normal file
View file

@ -0,0 +1,509 @@
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Brand styling */
.brand {
margin-bottom: 20px;
}
.app-name {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: #2c3e50;
margin-bottom: 8px;
font-size: 2.2rem;
font-weight: 600;
letter-spacing: -1px;
}
.tagline {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: #667eea;
font-size: 0.9rem;
font-weight: 500;
opacity: 0.8;
}
/* Welcome page styles */
.welcome-container {
text-align: center;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin-top: 100px;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
.username-form {
display: flex;
flex-direction: column;
gap: 15px;
margin: 30px 0;
}
.username-form input {
padding: 15px;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.username-form input:focus {
outline: none;
border-color: #667eea;
}
.username-form button, .btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
padding: 15px 20px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.username-form button:hover, .btn:hover {
transform: translateY(-2px);
}
.example {
color: #666;
font-size: 14px;
}
.example code {
background: #f8f9fa;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
/* Dashboard header */
.dashboard-header {
background: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.dashboard-header h1 {
color: #2c3e50;
font-size: 1.8rem;
}
.week-nav {
display: flex;
align-items: center;
gap: 15px;
}
.week-btn {
background: #f8f9fa;
color: #495057;
padding: 8px 16px;
border-radius: 6px;
text-decoration: none;
transition: background 0.2s;
}
.week-btn:hover {
background: #e9ecef;
}
.current-week {
font-weight: bold;
color: #667eea;
font-size: 1.1rem;
}
/* League scores - compact at top */
.scores-summary {
background: white;
border-radius: 12px;
padding: 15px;
margin-bottom: 30px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.score-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f1f3f4;
}
.score-row:last-child {
border-bottom: none;
}
.league-info {
display: flex;
align-items: center;
gap: 8px;
min-width: 140px;
}
.league-name {
font-weight: 600;
color: #2c3e50;
font-size: 14px;
}
/* League color dots */
.league-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
/* Different sizes for different contexts */
.league-info .league-dot {
width: 10px;
height: 10px;
}
.player-pill .league-dot {
width: 6px;
height: 6px;
}
.score-compact {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.user-name, .opp-name {
font-weight: 500;
color: #495057;
min-width: 80px;
}
.user-name {
text-align: right;
}
.score {
font-weight: bold;
color: #667eea;
min-width: 30px;
text-align: center;
}
.vs-compact {
color: #666;
font-size: 11px;
font-weight: 500;
}
/* Schedule section - main focus */
.schedule-section {
flex: 1;
}
.schedule-section h2 {
color: white;
margin-bottom: 25px;
font-size: 1.8rem;
text-align: center;
}
.calendar-rows {
max-width: 1200px;
margin: 0 auto;
}
/* Day row styling */
.day-row {
background: white;
border-radius: 16px;
margin-bottom: 20px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.day-header {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
padding: 15px 20px;
}
.day-header h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
}
.day-games {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 15px;
}
/* Game card styling */
.game-card {
background: #f8f9fa;
border-radius: 12px;
padding: 15px;
border: 1px solid #e9ecef;
transition: all 0.2s ease;
}
.game-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.game-info {
margin-bottom: 12px;
}
.game-time {
font-size: 12px;
color: #667eea;
margin-bottom: 8px;
font-weight: 600;
text-align: center;
}
.matchup {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
}
.away-team, .home-team {
color: #2c3e50;
}
.at {
color: #666;
font-size: 12px;
}
/* Player pills in calendar */
.game-players {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
margin-top: 12px;
min-height: 24px;
}
.player-pill {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 16px;
font-size: 11px;
border: 2px solid #dee2e6;
background: white;
font-weight: 500;
transition: all 0.2s ease;
}
.player-pill:hover {
transform: scale(1.05);
}
.player-pill .pos {
font-weight: bold;
font-size: 9px;
opacity: 0.8;
}
.player-pill .name {
font-weight: 600;
}
/* Position colors for pills */
.player-pill.qb {
border-color: rgb(252, 43, 109);
background: rgba(252, 43, 109, 0.1);
color: rgb(252, 43, 109);
}
.player-pill.rb {
border-color: rgb(0, 206, 184);
background: rgba(0, 206, 184, 0.1);
color: rgb(0, 206, 184);
}
.player-pill.wr {
border-color: rgb(0, 186, 255);
background: rgba(0, 186, 255, 0.1);
color: rgb(0, 186, 255);
}
.player-pill.te {
border-color: rgb(255, 174, 88);
background: rgba(255, 174, 88, 0.1);
color: rgb(255, 174, 88);
}
.no-games-week {
text-align: center;
color: white;
font-style: italic;
font-size: 16px;
padding: 60px 0;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
margin: 20px 0;
}
/* App footer */
.app-footer {
text-align: center;
padding: 20px;
margin-top: 40px;
}
.version {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
opacity: 0.8;
}
/* Error page */
.error-container {
text-align: center;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin-top: 100px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.error-container h1 {
color: #e74c3c;
margin-bottom: 20px;
}
.error-container p {
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
/* Mobile responsive styles */
@media (max-width: 768px) {
.container {
padding: 15px;
}
.dashboard-header {
flex-direction: column;
text-align: center;
}
.scores-summary {
padding: 12px;
}
.score-row {
flex-direction: column;
gap: 8px;
align-items: flex-start;
padding: 12px 0;
}
.league-info {
min-width: auto;
}
.score-compact {
align-self: stretch;
justify-content: space-between;
}
.day-games {
grid-template-columns: 1fr;
gap: 12px;
padding: 15px;
}
.day-header {
padding: 12px 15px;
}
.day-header h3 {
font-size: 1.1rem;
}
.schedule-section h2 {
font-size: 1.5rem;
}
.welcome-container {
margin-top: 50px;
padding: 30px 20px;
}
.app-name {
font-size: 1.6rem;
}
}
@media (max-width: 480px) {
.username-form {
gap: 12px;
}
.username-form input,
.username-form button {
padding: 12px;
font-size: 14px;
}
.week-nav {
flex-direction: column;
gap: 10px;
}
}

57
templates/base.html Normal file
View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}FantasyCron{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
<!-- App version footer -->
<footer class="app-footer">
<div class="version">FantasyCron v{{ app_version }}</div>
</footer>
<script>
function refreshScores() {
// Get current page info
const username = document.querySelector('meta[name="username"]')?.content;
const week = document.querySelector('meta[name="week"]')?.content;
if (username && week) {
// Fetch updated scores from API
fetch(`/api/refresh/${username}/${week}`)
.then(response => response.json())
.then(data => {
if (data.league_scores) {
// Update score displays
data.league_scores.forEach(league => {
const userScore = document.getElementById(`user-score-${league.league_id}`);
const oppScore = document.getElementById(`opp-score-${league.league_id}`);
if (userScore) userScore.textContent = league.user_points.toFixed(1);
if (oppScore) oppScore.textContent = league.opponent_points.toFixed(1);
});
}
})
.catch(error => console.log('Refresh failed:', error));
}
}
// Set refresh interval based on game day
const now = new Date();
const dayOfWeek = now.getDay();
const isGameTime = dayOfWeek === 4 || dayOfWeek === 0 || dayOfWeek === 1; // Thu, Sun, Mon
if (isGameTime) {
setInterval(refreshScores, 60000); // Every minute during games
} else {
setInterval(refreshScores, 21600000); // Every 6 hours otherwise
}
</script>
</body>
</html>

102
templates/dashboard.html Normal file
View file

@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}{{ user.display_name }} - Week {{ week }}{% endblock %}
{% block content %}
<!-- Meta tags for JavaScript -->
<meta name="username" content="{{ user.username }}">
<meta name="week" content="{{ week }}">
<!-- Header with user name and week navigation -->
<header class="dashboard-header">
<h1>{{ user.display_name }}</h1>
<div class="week-nav">
<a href="/{{ user.username }}/{{ week - 1 }}" class="week-btn">&larr; Week {{ week - 1 }}</a>
<span class="current-week">Week {{ week }}</span>
<a href="/{{ user.username }}/{{ week + 1 }}" class="week-btn">Week {{ week + 1 }} &rarr;</a>
</div>
</header>
<!-- Compact league scores at top -->
<section class="scores-summary">
{% for league_info in league_data %}
<div class="score-row">
<div class="league-info">
<!-- League color dot -->
<span class="league-dot" style="background-color: {{ league_info.league_color }};"></span>
<span class="league-name">{{ league_info.league.name }}</span>
</div>
<div class="score-compact">
<span class="user-name">{{ user.display_name }}</span>
<span class="score" id="user-score-{{ league_info.league.league_id }}">
{{ league_info.user_matchup.points|round(1) if league_info.user_matchup else '0.0' }}
</span>
<span class="vs-compact">vs</span>
<span class="score" id="opp-score-{{ league_info.league.league_id }}">
{{ league_info.opponent_matchup.points|round(1) if league_info.opponent_matchup else '0.0' }}
</span>
<span class="opp-name">{{ league_info.opponent_user.display_name if league_info.opponent_user else 'Opponent' }}</span>
</div>
</div>
{% endfor %}
</section>
<!-- Main calendar section -->
<section class="schedule-section">
<h2>Week {{ week }} Games</h2>
<div class="calendar-rows">
<!-- Check if schedule has games -->
{% set has_games = false %}
{% for day, games in schedule.items() if games %}
{% set has_games = true %}
{% endfor %}
<!-- Loop through days that have games -->
{% for day, games in schedule.items() if games %}
<div class="day-row">
<div class="day-header">
<h3>{{ day }}</h3>
</div>
<div class="day-games">
<!-- Games for this day -->
{% for game in games %}
<div class="game-card">
<div class="game-info">
<div class="game-time">{{ game.time }}</div>
<div class="matchup">
<span class="away-team">{{ game.away_team }}</span>
<span class="at">@</span>
<span class="home-team">{{ game.home_team }}</span>
</div>
</div>
<!-- Show user's players in this game -->
<div class="game-players">
{% for league_info in league_data %}
{% for player in league_info.all_players %}
{% if player.team in game.teams %}
<div class="player-pill {{ player.fantasy_positions[0]|lower if player.fantasy_positions else 'flex' }}">
<!-- League color dot -->
<span class="league-dot" style="background-color: {{ league_info.league_color }};"></span>
<span class="pos">{{ player.fantasy_positions[0] if player.fantasy_positions else 'FLEX' }}</span>
<span class="name">{{ player.last_name }}</span>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<!-- Show message if no games found -->
{% if not has_games %}
<div class="no-games-week">
<p>No games found for week {{ week }}</p>
</div>
{% endif %}
</div>
</section>
{% endblock %}

11
templates/error.html Normal file
View file

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Error{% endblock %}
{% block content %}
<div class="error-container">
<h1>Error</h1>
<p>{{ message }}</p>
<a href="/" class="btn">Back to Home</a>
</div>
{% endblock %}

33
templates/index.html Normal file
View file

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}FantasyCron - Enter Username{% endblock %}
{% block content %}
<div class="welcome-container">
<div class="brand">
<h1 class="app-name">FantasyCron</h1>
<div class="tagline">* * * * 0,1,4</div>
</div>
<p>Enter your Sleeper username to view your fantasy teams</p>
<!-- Username input form -->
<form class="username-form" onsubmit="goToUser(event)">
<input type="text" id="username" placeholder="Enter Sleeper username" required>
<button type="submit">View Dashboard</button>
</form>
<div class="example">
<p>Example: <code>sleeperuser</code></p>
</div>
</div>
<script>
function goToUser(event) {
event.preventDefault(); // Prevent form submission
const username = document.getElementById('username').value.trim();
if (username) {
window.location.href = `/${username}`; // Navigate to user dashboard
}
}
</script>
{% endblock %}