""" 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 Request body (optional): { "max_articles": 10 // Number of articles per feed } """ 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 > 100: return jsonify({ 'success': False, 'error': 'max_articles must be an integer between 1 and 100' }), 400 # Execute crawler in crawler container using docker exec try: result = subprocess.run( ['docker', 'exec', 'munich-news-crawler', 'python', 'crawler_service.py', str(max_articles)], capture_output=True, text=True, timeout=300 # 5 minute timeout ) # Check result success = result.returncode == 0 return jsonify({ 'success': success, 'message': f'Crawler {"executed successfully" if success else "failed"}', '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': 'Crawler timed out after 5 minutes' }), 500 except Exception as e: return jsonify({ 'success': False, 'error': f'Failed to run crawler: {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