Initial commit
This commit is contained in:
commit
315e731234
27 changed files with 3403 additions and 0 deletions
382
app.py
Normal file
382
app.py
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
from flask import Flask, render_template, jsonify, request, redirect, url_for, flash
|
||||
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from blog_manager import BlogManager
|
||||
from forms import LoginForm, BlogPostForm
|
||||
from user import User
|
||||
|
||||
# Import traffic tracking components
|
||||
from models import db
|
||||
from traffic_tracker import TrafficTracker
|
||||
from sqlalchemy import func, desc
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-key-change-in-production')
|
||||
|
||||
# SQLite Configuration for traffic tracking
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(basedir, "traffic_analytics.db")}'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
tracker = TrafficTracker(app)
|
||||
|
||||
# Initialize Flask-Login
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'admin_login'
|
||||
login_manager.login_message = 'Please log in to access the admin panel.'
|
||||
|
||||
# Initialize Blog Manager
|
||||
blog_manager = BlogManager()
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.get(user_id)
|
||||
|
||||
|
||||
# Initialize Strava service (with error handling)
|
||||
try:
|
||||
from strava_service import StravaService
|
||||
|
||||
strava = StravaService()
|
||||
STRAVA_ENABLED = True
|
||||
except Exception as e:
|
||||
print(f"Strava service not available: {e}")
|
||||
strava = None
|
||||
STRAVA_ENABLED = False
|
||||
|
||||
|
||||
# Load blog posts from BlogManager
|
||||
def load_blog_posts():
|
||||
return blog_manager.load_posts()
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
posts = load_blog_posts()
|
||||
recent_posts = sorted(posts, key=lambda x: x['date'], reverse=True)[:3]
|
||||
return render_template('index.html', recent_posts=recent_posts)
|
||||
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
return render_template('about.html')
|
||||
|
||||
|
||||
@app.route('/hardware')
|
||||
def hardware():
|
||||
return render_template('hardware.html')
|
||||
|
||||
|
||||
@app.route('/biking')
|
||||
def biking():
|
||||
ytd_stats = None
|
||||
recent_activities = []
|
||||
|
||||
if STRAVA_ENABLED and strava:
|
||||
try:
|
||||
ytd_stats = strava.get_ytd_stats()
|
||||
recent_activities = strava.format_recent_activities()
|
||||
except Exception as e:
|
||||
print(f"Error fetching Strava data: {e}")
|
||||
ytd_stats = {
|
||||
'distance': 0,
|
||||
'count': 0,
|
||||
'elevation': 0,
|
||||
'time': 0
|
||||
}
|
||||
else:
|
||||
ytd_stats = {
|
||||
'distance': 2450,
|
||||
'count': 127,
|
||||
'elevation': 45600,
|
||||
'time': 156
|
||||
}
|
||||
recent_activities = [
|
||||
{
|
||||
'name': 'Morning Training Ride',
|
||||
'distance': 25.3,
|
||||
'elevation': 1200,
|
||||
'time': '1h 45m',
|
||||
'date': 'January 5, 2025'
|
||||
}
|
||||
]
|
||||
|
||||
current_time = datetime.now().strftime('%B %d, %Y at %I:%M %p')
|
||||
|
||||
return render_template('biking.html',
|
||||
ytd_stats=ytd_stats,
|
||||
recent_activities=recent_activities,
|
||||
strava_enabled=STRAVA_ENABLED,
|
||||
last_updated=current_time)
|
||||
|
||||
|
||||
@app.route('/blog')
|
||||
def blog():
|
||||
posts = load_blog_posts()
|
||||
posts = sorted(posts, key=lambda x: x['date'], reverse=True)
|
||||
return render_template('blog.html', posts=posts)
|
||||
|
||||
|
||||
@app.route('/blog/<int:post_id>')
|
||||
def blog_post(post_id):
|
||||
post = blog_manager.get_post(post_id)
|
||||
if not post:
|
||||
return "Post not found", 404
|
||||
return render_template('blog_post.html', post=post)
|
||||
|
||||
|
||||
# Admin Routes
|
||||
@app.route('/admin/login', methods=['GET', 'POST'])
|
||||
def admin_login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.authenticate(form.username.data, form.password.data)
|
||||
if user:
|
||||
login_user(user)
|
||||
flash('Logged in successfully!', 'success')
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
else:
|
||||
flash('Invalid username or password.', 'error')
|
||||
|
||||
return render_template('admin/login.html', form=form)
|
||||
|
||||
|
||||
@app.route('/admin/logout')
|
||||
@login_required
|
||||
def admin_logout():
|
||||
logout_user()
|
||||
flash('You have been logged out.', 'info')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/admin')
|
||||
@login_required
|
||||
def admin_dashboard():
|
||||
posts = load_blog_posts()
|
||||
posts = sorted(posts, key=lambda x: x['date'], reverse=True)
|
||||
return render_template('admin/dashboard.html', posts=posts)
|
||||
|
||||
|
||||
@app.route('/admin/post/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin_new_post():
|
||||
form = BlogPostForm()
|
||||
if form.validate_on_submit():
|
||||
blog_manager.create_post(
|
||||
title=form.title.data,
|
||||
content=form.content.data,
|
||||
category=form.category.data,
|
||||
excerpt=form.excerpt.data or None
|
||||
)
|
||||
flash('Post created successfully!', 'success')
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
|
||||
return render_template('admin/edit_post.html', form=form, title='New Post')
|
||||
|
||||
|
||||
@app.route('/admin/post/<int:post_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin_edit_post(post_id):
|
||||
post = blog_manager.get_post(post_id)
|
||||
if not post:
|
||||
flash('Post not found.', 'error')
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
|
||||
form = BlogPostForm(obj=post)
|
||||
if form.validate_on_submit():
|
||||
blog_manager.update_post(
|
||||
post_id=post_id,
|
||||
title=form.title.data,
|
||||
content=form.content.data,
|
||||
category=form.category.data,
|
||||
excerpt=form.excerpt.data or None
|
||||
)
|
||||
flash('Post updated successfully!', 'success')
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
|
||||
return render_template('admin/edit_post.html', form=form, post=post, title='Edit Post')
|
||||
|
||||
|
||||
@app.route('/admin/post/<int:post_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def admin_delete_post(post_id):
|
||||
blog_manager.delete_post(post_id)
|
||||
flash('Post deleted successfully!', 'success')
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
|
||||
|
||||
# Traffic Analytics Admin Route
|
||||
@app.route('/admin/traffic')
|
||||
@login_required
|
||||
def admin_traffic():
|
||||
from models import PageView, UniqueVisitor
|
||||
|
||||
# Get date range from query params (default to last 30 days)
|
||||
days = int(request.args.get('days', 30))
|
||||
end_date = datetime.utcnow().date()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# Basic stats
|
||||
total_views = PageView.query.filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date)
|
||||
).count()
|
||||
|
||||
unique_visitors = db.session.query(func.count(func.distinct(PageView.ip_address))).filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date)
|
||||
).scalar()
|
||||
|
||||
avg_response_time = db.session.query(func.avg(PageView.response_time)).filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date)
|
||||
).scalar() or 0
|
||||
|
||||
# Calculate bounce rate (sessions with only one page view)
|
||||
single_page_sessions = db.session.query(
|
||||
PageView.session_id
|
||||
).filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date)
|
||||
).group_by(PageView.session_id).having(func.count(PageView.id) == 1).count()
|
||||
|
||||
total_sessions = db.session.query(
|
||||
func.count(func.distinct(PageView.session_id))
|
||||
).filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date)
|
||||
).scalar()
|
||||
|
||||
bounce_rate = (single_page_sessions / total_sessions * 100) if total_sessions > 0 else 0
|
||||
|
||||
# Top pages
|
||||
top_pages = db.session.query(
|
||||
PageView.path,
|
||||
func.count(PageView.id).label('views')
|
||||
).filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date)
|
||||
).group_by(PageView.path).order_by(desc('views')).limit(10).all()
|
||||
|
||||
# Top referrers
|
||||
top_referrers = db.session.query(
|
||||
PageView.referrer,
|
||||
func.count(PageView.id).label('views')
|
||||
).filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date),
|
||||
PageView.referrer.isnot(None),
|
||||
PageView.referrer != ''
|
||||
).group_by(PageView.referrer).order_by(desc('views')).limit(10).all()
|
||||
|
||||
# Daily views for chart - Fixed to handle SQLite date strings
|
||||
daily_views_raw = db.session.query(
|
||||
func.date(PageView.timestamp).label('date'),
|
||||
func.count(PageView.id).label('views'),
|
||||
func.count(func.distinct(PageView.ip_address)).label('unique_visitors')
|
||||
).filter(
|
||||
func.date(PageView.timestamp).between(start_date, end_date)
|
||||
).group_by(func.date(PageView.timestamp)).order_by('date').all()
|
||||
|
||||
# Recent activity (last 50 page views)
|
||||
recent_activity = PageView.query.filter(
|
||||
PageView.timestamp >= datetime.utcnow() - timedelta(hours=24)
|
||||
).order_by(desc(PageView.timestamp)).limit(50).all()
|
||||
|
||||
stats = {
|
||||
'total_views': total_views,
|
||||
'unique_visitors': unique_visitors,
|
||||
'avg_response_time': round(avg_response_time, 2),
|
||||
'bounce_rate': round(bounce_rate, 2)
|
||||
}
|
||||
|
||||
# Format daily views for JSON - Handle both string and date objects
|
||||
daily_views_json = []
|
||||
for day in daily_views_raw:
|
||||
# Handle case where SQLite returns date as string
|
||||
if isinstance(day.date, str):
|
||||
date_str = day.date
|
||||
else:
|
||||
date_str = day.date.isoformat()
|
||||
|
||||
daily_views_json.append({
|
||||
'date': date_str,
|
||||
'views': day.views,
|
||||
'unique_visitors': day.unique_visitors
|
||||
})
|
||||
|
||||
return render_template('admin/traffic.html',
|
||||
stats=stats,
|
||||
top_pages=top_pages,
|
||||
top_referrers=top_referrers,
|
||||
daily_views=daily_views_json,
|
||||
recent_activity=recent_activity,
|
||||
days=days)
|
||||
|
||||
# Real-time traffic API for admin dashboard
|
||||
@app.route('/admin/traffic/api/realtime')
|
||||
@login_required
|
||||
def realtime_traffic():
|
||||
from models import PageView
|
||||
|
||||
# Last 5 minutes of traffic
|
||||
five_min_ago = datetime.utcnow() - timedelta(minutes=5)
|
||||
|
||||
recent_views = PageView.query.filter(
|
||||
PageView.timestamp >= five_min_ago
|
||||
).order_by(desc(PageView.timestamp)).limit(50).all()
|
||||
|
||||
active_users = db.session.query(func.count(func.distinct(PageView.session_id))).filter(
|
||||
PageView.timestamp >= five_min_ago
|
||||
).scalar()
|
||||
|
||||
return jsonify({
|
||||
'active_users': active_users,
|
||||
'recent_views': [{
|
||||
'path': view.path,
|
||||
'timestamp': view.timestamp.isoformat(),
|
||||
'ip_address': view.ip_address[:8] + '...', # Partial IP for privacy
|
||||
'country': view.country,
|
||||
'city': view.city
|
||||
} for view in recent_views]
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/strava-stats')
|
||||
def strava_stats_api():
|
||||
if not STRAVA_ENABLED or not strava:
|
||||
return jsonify({
|
||||
'error': 'Strava service not available',
|
||||
'ytd_stats': None,
|
||||
'recent_activities': []
|
||||
}), 503
|
||||
|
||||
try:
|
||||
ytd_stats = strava.get_ytd_stats()
|
||||
recent_activities = strava.format_recent_activities()
|
||||
|
||||
return jsonify({
|
||||
'ytd_stats': ytd_stats,
|
||||
'recent_activities': recent_activities
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': str(e),
|
||||
'ytd_stats': None,
|
||||
'recent_activities': []
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
return {'status': 'healthy', 'timestamp': datetime.utcnow().isoformat()}
|
||||
|
||||
|
||||
# Create database tables on startup
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
Loading…
Add table
Add a link
Reference in a new issue