304 lines
10 KiB
Python
304 lines
10 KiB
Python
"""
|
|
Admin routes for testing and manual operations
|
|
"""
|
|
from flask import Blueprint, request, jsonify
|
|
import subprocess
|
|
import os
|
|
from pathlib import Path
|
|
|
|
admin_bp = Blueprint('admin', __name__)
|
|
|
|
|
|
@admin_bp.route('/api/admin/trigger-crawl', methods=['POST'])
|
|
def trigger_crawl():
|
|
"""
|
|
Manually trigger the news crawler asynchronously via Redis queue
|
|
Uses Redis message queue for non-blocking execution
|
|
|
|
Request body (optional):
|
|
{
|
|
"max_articles": 10 // Number of articles per feed
|
|
}
|
|
"""
|
|
try:
|
|
import redis
|
|
import json
|
|
|
|
# Handle both JSON and empty body
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
except:
|
|
data = {}
|
|
|
|
max_articles = data.get('max_articles', 10)
|
|
|
|
# Validate max_articles
|
|
if not isinstance(max_articles, int) or max_articles < 1 or max_articles > 100:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'max_articles must be an integer between 1 and 100'
|
|
}), 400
|
|
|
|
# Get Redis client
|
|
redis_url = os.getenv('REDIS_URL', 'redis://redis:6379')
|
|
r = redis.from_url(redis_url, decode_responses=True)
|
|
|
|
# Publish message to Redis queue
|
|
message = {
|
|
'task': 'crawl_news',
|
|
'max_articles': max_articles,
|
|
'timestamp': str(os.times())
|
|
}
|
|
r.lpush('news_crawl_queue', json.dumps(message))
|
|
|
|
# Return immediately without waiting
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'News crawl task queued',
|
|
'max_articles': max_articles
|
|
}), 202 # 202 Accepted
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Failed to queue news crawl: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@admin_bp.route('/api/admin/send-test-email', methods=['POST'])
|
|
def send_test_email():
|
|
"""
|
|
Send a test newsletter to a specific email
|
|
|
|
Request body:
|
|
{
|
|
"email": "test@example.com",
|
|
"max_articles": 10 // Optional, defaults to 10
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data or 'email' not in data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Email address is required'
|
|
}), 400
|
|
|
|
email = data.get('email', '').strip()
|
|
max_articles = data.get('max_articles', 10)
|
|
|
|
# Validate email
|
|
if not email or '@' not in email:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Invalid email address'
|
|
}), 400
|
|
|
|
# Validate max_articles (not used currently but validated for future use)
|
|
if not isinstance(max_articles, int) or max_articles < 1 or max_articles > 50:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'max_articles must be an integer between 1 and 50'
|
|
}), 400
|
|
|
|
# Execute sender in sender container using docker exec
|
|
try:
|
|
result = subprocess.run(
|
|
['docker', 'exec', 'munich-news-sender', 'python', 'sender_service.py', 'test', email],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60 # 1 minute timeout
|
|
)
|
|
|
|
# Check if successful
|
|
success = result.returncode == 0
|
|
|
|
return jsonify({
|
|
'success': success,
|
|
'message': f'Test email {"sent" if success else "failed"} to {email}',
|
|
'email': email,
|
|
'output': result.stdout[-1000:] if result.stdout else '', # Last 1000 chars
|
|
'errors': result.stderr[-500:] if result.stderr else ''
|
|
}), 200 if success else 500
|
|
|
|
except FileNotFoundError:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Docker command not found. Make sure Docker is installed and the socket is mounted.'
|
|
}), 500
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Email sending timed out after 1 minute'
|
|
}), 500
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Failed to send email: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@admin_bp.route('/api/admin/send-newsletter', methods=['POST'])
|
|
def send_newsletter():
|
|
"""
|
|
Send newsletter to all active subscribers
|
|
|
|
Request body (optional):
|
|
{
|
|
"max_articles": 10 // Optional, defaults to 10
|
|
}
|
|
"""
|
|
try:
|
|
# Handle both JSON and empty body
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
except:
|
|
data = {}
|
|
|
|
max_articles = data.get('max_articles', 10)
|
|
|
|
# Validate max_articles
|
|
if not isinstance(max_articles, int) or max_articles < 1 or max_articles > 50:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'max_articles must be an integer between 1 and 50'
|
|
}), 400
|
|
|
|
# Get subscriber count first
|
|
from database import subscribers_collection
|
|
subscriber_count = subscribers_collection.count_documents({'status': 'active'})
|
|
|
|
if subscriber_count == 0:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'No active subscribers found',
|
|
'subscriber_count': 0
|
|
}), 400
|
|
|
|
# Execute sender in sender container using docker exec
|
|
try:
|
|
result = subprocess.run(
|
|
['docker', 'exec', 'munich-news-sender', 'python', 'sender_service.py', 'send', str(max_articles)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300 # 5 minute timeout for multiple emails
|
|
)
|
|
|
|
# Check if successful
|
|
success = result.returncode == 0
|
|
|
|
return jsonify({
|
|
'success': success,
|
|
'message': f'Newsletter {"sent successfully" if success else "failed"} to {subscriber_count} subscribers',
|
|
'subscriber_count': subscriber_count,
|
|
'max_articles': max_articles,
|
|
'output': result.stdout[-1000:] if result.stdout else '', # Last 1000 chars
|
|
'errors': result.stderr[-500:] if result.stderr else ''
|
|
}), 200 if success else 500
|
|
|
|
except FileNotFoundError:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Docker command not found. Make sure Docker is installed and the socket is mounted.'
|
|
}), 500
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Newsletter sending timed out after 5 minutes'
|
|
}), 500
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Failed to send newsletter: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@admin_bp.route('/api/admin/recent-articles', methods=['GET'])
|
|
def get_recent_articles():
|
|
"""Get recently summarized articles"""
|
|
try:
|
|
from database import articles_collection
|
|
|
|
# Get last 10 articles with summaries, sorted by when they were summarized
|
|
articles = list(articles_collection.find(
|
|
{'summary': {'$exists': True, '$ne': None}},
|
|
{
|
|
'title': 1,
|
|
'title_en': 1,
|
|
'source': 1,
|
|
'category': 1,
|
|
'summarized_at': 1,
|
|
'created_at': 1,
|
|
'summary_word_count': 1,
|
|
'_id': 0
|
|
}
|
|
).sort('summarized_at', -1).limit(10))
|
|
|
|
# Convert datetime to ISO format
|
|
for article in articles:
|
|
if 'summarized_at' in article and article['summarized_at']:
|
|
article['summarized_at'] = article['summarized_at'].isoformat()
|
|
if 'created_at' in article and article['created_at']:
|
|
article['created_at'] = article['created_at'].isoformat()
|
|
|
|
return jsonify({
|
|
'articles': articles,
|
|
'total': len(articles)
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@admin_bp.route('/api/admin/stats', methods=['GET'])
|
|
def get_stats():
|
|
"""Get system statistics"""
|
|
try:
|
|
from database import (
|
|
articles_collection,
|
|
subscribers_collection,
|
|
rss_feeds_collection,
|
|
newsletter_sends_collection,
|
|
link_clicks_collection
|
|
)
|
|
|
|
stats = {
|
|
'articles': {
|
|
'total': articles_collection.count_documents({}),
|
|
'with_summary': articles_collection.count_documents({'summary': {'$exists': True, '$ne': None}}),
|
|
'today': articles_collection.count_documents({
|
|
'crawled_at': {
|
|
'$gte': datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
}
|
|
})
|
|
},
|
|
'subscribers': {
|
|
'total': subscribers_collection.count_documents({}),
|
|
'active': subscribers_collection.count_documents({'status': 'active'})
|
|
},
|
|
'rss_feeds': {
|
|
'total': rss_feeds_collection.count_documents({}),
|
|
'active': rss_feeds_collection.count_documents({'active': True})
|
|
},
|
|
'tracking': {
|
|
'total_sends': newsletter_sends_collection.count_documents({}),
|
|
'total_opens': newsletter_sends_collection.count_documents({'opened': True}),
|
|
'total_clicks': link_clicks_collection.count_documents({'clicked': True})
|
|
}
|
|
}
|
|
|
|
return jsonify(stats), 200
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
|
|
# Import datetime for stats endpoint
|
|
from datetime import datetime
|