update
This commit is contained in:
@@ -2,7 +2,21 @@ FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
# Install Docker CLI for admin endpoints
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
&& install -m 0755 -d /etc/apt/keyrings \
|
||||
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
|
||||
&& chmod a+r /etc/apt/keyrings/docker.asc \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends docker-ce-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from routes.ollama_routes import ollama_bp
|
||||
from routes.newsletter_routes import newsletter_bp
|
||||
from routes.tracking_routes import tracking_bp
|
||||
from routes.analytics_routes import analytics_bp
|
||||
from routes.admin_routes import admin_bp
|
||||
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__)
|
||||
@@ -25,6 +26,7 @@ app.register_blueprint(ollama_bp)
|
||||
app.register_blueprint(newsletter_bp)
|
||||
app.register_blueprint(tracking_bp)
|
||||
app.register_blueprint(analytics_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
|
||||
# Health check endpoint
|
||||
@app.route('/health')
|
||||
|
||||
193
backend/routes/admin_routes.py
Normal file
193
backend/routes/admin_routes.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
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:
|
||||
data = request.get_json() or {}
|
||||
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/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({'active': True})
|
||||
},
|
||||
'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
|
||||
Reference in New Issue
Block a user