Zelyanne commited on
Commit
a5f9ee8
·
unverified ·
2 Parent(s): f97685e 9b1e36a

Merge pull request #3 from Zelyanne/main

Browse files
backend/api/posts.py CHANGED
@@ -7,6 +7,7 @@ from flask_jwt_extended import jwt_required, get_jwt_identity
7
  from backend.services.content_service import ContentService
8
  from backend.services.linkedin_service import LinkedInService
9
  from backend.utils.image_utils import ensure_bytes_format
 
10
 
11
  posts_bp = Blueprint('posts', __name__)
12
 
@@ -107,16 +108,15 @@ def _generate_post_task(user_id, job_id, job_store, hugging_key):
107
  Args:
108
  user_id (str): User ID for personalization
109
  job_id (str): Job ID to update status in job store
110
- job_store (dict): Job store dictionary
111
  hugging_key (str): Hugging Face API key
112
  """
113
  try:
 
 
 
114
  # Update job status to processing
115
- job_store[job_id] = {
116
- 'status': 'processing',
117
- 'result': None,
118
- 'error': None
119
- }
120
 
121
  # Generate content using content service
122
  # Pass the Hugging Face key directly to the service
@@ -136,24 +136,23 @@ def _generate_post_task(user_id, job_id, job_store, hugging_key):
136
  image_data = None
137
 
138
  # Update job status to completed with result
139
- job_store[job_id] = {
140
- 'status': 'completed',
141
- 'result': {
142
  'content': generated_content,
143
  'image_data': image_data # This could be bytes or a URL string
144
- },
145
- 'error': None
146
- }
147
 
148
  except Exception as e:
149
  error_message = str(e)
150
  safe_log_message(f"Generate post background task error: {error_message}")
151
  # Update job status to failed with error
152
- job_store[job_id] = {
153
- 'status': 'failed',
154
- 'result': None,
155
- 'error': error_message
156
- }
157
 
158
  @posts_bp.route('/generate', methods=['POST'])
159
  @jwt_required()
@@ -184,12 +183,10 @@ def generate_post():
184
  # Create a job ID
185
  job_id = str(uuid.uuid4())
186
 
187
- # Initialize job status
188
- current_app.job_store[job_id] = {
189
- 'status': 'pending',
190
- 'result': None,
191
- 'error': None
192
- }
193
 
194
  # Get Hugging Face key
195
  hugging_key = current_app.config['HUGGING_KEY']
@@ -227,8 +224,10 @@ def get_job_status(job_id):
227
  JSON: Job status and result if completed
228
  """
229
  try:
230
- # Get job from store
231
- job = current_app.job_store.get(job_id)
 
 
232
 
233
  if not job:
234
  return jsonify({
@@ -284,8 +283,12 @@ def get_job_status(job_id):
284
  elif isinstance(image_data, str):
285
  # Check if it's a local file path
286
  if os.path.exists(image_data):
287
- # Store the file path in job store for later retrieval
288
- job['image_file_path'] = image_data
 
 
 
 
289
  # Generate absolute URL instead of relative URL
290
  base_url = request.host_url.rstrip('/')
291
  response_data['image_url'] = f"{base_url}/api/posts/image/{job_id}" # API endpoint to serve the image
@@ -330,8 +333,10 @@ def get_job_image(job_id):
330
  Image file
331
  """
332
  try:
333
- # Get job from store
334
- job = current_app.job_store.get(job_id)
 
 
335
 
336
  if not job:
337
  return jsonify({
@@ -340,7 +345,7 @@ def get_job_image(job_id):
340
  }), 404
341
 
342
  # Check if job has an image file path
343
- image_file_path = job.get('image_file_path')
344
  if not image_file_path or not os.path.exists(image_file_path):
345
  return jsonify({
346
  'success': False,
 
7
  from backend.services.content_service import ContentService
8
  from backend.services.linkedin_service import LinkedInService
9
  from backend.utils.image_utils import ensure_bytes_format
10
+ from backend.utils.redis_job_store import get_redis_job_store
11
 
12
  posts_bp = Blueprint('posts', __name__)
13
 
 
108
  Args:
109
  user_id (str): User ID for personalization
110
  job_id (str): Job ID to update status in job store
111
+ job_store (dict): Job store dictionary (kept for backward compatibility, but now using Redis)
112
  hugging_key (str): Hugging Face API key
113
  """
114
  try:
115
+ # Get Redis job store
116
+ redis_job_store = get_redis_job_store()
117
+
118
  # Update job status to processing
119
+ redis_job_store.update_job(job_id, status='processing')
 
 
 
 
120
 
121
  # Generate content using content service
122
  # Pass the Hugging Face key directly to the service
 
136
  image_data = None
137
 
138
  # Update job status to completed with result
139
+ redis_job_store.update_job(job_id,
140
+ status='completed',
141
+ result={
142
  'content': generated_content,
143
  'image_data': image_data # This could be bytes or a URL string
144
+ }
145
+ )
 
146
 
147
  except Exception as e:
148
  error_message = str(e)
149
  safe_log_message(f"Generate post background task error: {error_message}")
150
  # Update job status to failed with error
151
+ redis_job_store = get_redis_job_store()
152
+ redis_job_store.update_job(job_id,
153
+ status='failed',
154
+ error=error_message
155
+ )
156
 
157
  @posts_bp.route('/generate', methods=['POST'])
158
  @jwt_required()
 
183
  # Create a job ID
184
  job_id = str(uuid.uuid4())
185
 
186
+ # Initialize job status in Redis store
187
+ from backend.utils.redis_job_store import get_redis_job_store
188
+ redis_job_store = get_redis_job_store()
189
+ redis_job_store.create_job(job_id, 'pending')
 
 
190
 
191
  # Get Hugging Face key
192
  hugging_key = current_app.config['HUGGING_KEY']
 
224
  JSON: Job status and result if completed
225
  """
226
  try:
227
+ # Get job from Redis store
228
+ from backend.utils.redis_job_store import get_redis_job_store
229
+ redis_job_store = get_redis_job_store()
230
+ job = redis_job_store.get_job(job_id)
231
 
232
  if not job:
233
  return jsonify({
 
283
  elif isinstance(image_data, str):
284
  # Check if it's a local file path
285
  if os.path.exists(image_data):
286
+ # Store the file path in Redis job store for later retrieval
287
+ redis_job_store.update_job(job_id, result={
288
+ 'content': job['result']['content'],
289
+ 'image_data': image_data,
290
+ 'image_file_path': image_data
291
+ })
292
  # Generate absolute URL instead of relative URL
293
  base_url = request.host_url.rstrip('/')
294
  response_data['image_url'] = f"{base_url}/api/posts/image/{job_id}" # API endpoint to serve the image
 
333
  Image file
334
  """
335
  try:
336
+ # Get job from Redis store
337
+ from backend.utils.redis_job_store import get_redis_job_store
338
+ redis_job_store = get_redis_job_store()
339
+ job = redis_job_store.get_job(job_id)
340
 
341
  if not job:
342
  return jsonify({
 
345
  }), 404
346
 
347
  # Check if job has an image file path
348
+ image_file_path = job.get('image_file_path') if job else None
349
  if not image_file_path or not os.path.exists(image_file_path):
350
  return jsonify({
351
  'success': False,
backend/app.py CHANGED
@@ -165,9 +165,19 @@ def create_app():
165
  # Initialize Supabase client
166
  app.supabase = init_supabase(app.config['SUPABASE_URL'], app.config['SUPABASE_KEY'])
167
 
168
- # Initialize a simple in-memory job store for tracking async tasks
169
- # In production, you'd use a database or Redis for this
170
- app.job_store = {}
 
 
 
 
 
 
 
 
 
 
171
 
172
  # Initialize a ThreadPoolExecutor for running background tasks
173
  # In production, you'd use a proper task scheduler like APScheduler
 
165
  # Initialize Supabase client
166
  app.supabase = init_supabase(app.config['SUPABASE_URL'], app.config['SUPABASE_KEY'])
167
 
168
+ # Initialize Redis-based job store for tracking async tasks across multiple Gunicorn workers
169
+ try:
170
+ from backend.utils.redis_job_store import RedisJobStore
171
+ redis_url = app.config.get('REDIS_URL', 'redis://localhost:6379/0')
172
+ # Use configurable TTL from environment, default to 24 hours
173
+ job_ttl_hours = int(app.config.get('JOB_TTL_HOURS', 24))
174
+ app.redis_job_store = RedisJobStore(redis_url, default_ttl_hours=job_ttl_hours)
175
+ app.logger.info("Redis job store initialized successfully")
176
+ except Exception as e:
177
+ app.logger.error(f"Failed to initialize Redis job store: {str(e)}")
178
+ # Fallback to in-memory store if Redis is not available
179
+ app.job_store = {}
180
+ app.logger.warning("Using in-memory job store as fallback - not suitable for multi-worker production")
181
 
182
  # Initialize a ThreadPoolExecutor for running background tasks
183
  # In production, you'd use a proper task scheduler like APScheduler
backend/config.py CHANGED
@@ -79,6 +79,9 @@ class Config:
79
  LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO' if ENVIRONMENT == 'production' else 'DEBUG')
80
  UNICODE_SAFE_LOGGING = UNICODE_LOGGING and not IS_WINDOWS
81
 
 
 
 
82
  # Celery configuration
83
  CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
84
  CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
 
79
  LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO' if ENVIRONMENT == 'production' else 'DEBUG')
80
  UNICODE_SAFE_LOGGING = UNICODE_LOGGING and not IS_WINDOWS
81
 
82
+ # Redis configuration for job store
83
+ REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
84
+
85
  # Celery configuration
86
  CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
87
  CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
backend/utils/redis_job_store.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Redis-based job store for distributed job tracking across multiple Gunicorn workers.
3
+ """
4
+ import json
5
+ import uuid
6
+ import redis
7
+ from flask import current_app
8
+ from datetime import datetime, timedelta
9
+ import logging
10
+ import re
11
+
12
+
13
+ class RedisJobStore:
14
+ """
15
+ Redis-based job store for tracking background tasks across multiple Gunicorn workers.
16
+ """
17
+
18
+ # Valid job statuses
19
+ VALID_STATUSES = {'pending', 'processing', 'completed', 'failed', 'cancelled'}
20
+
21
+ def __init__(self, redis_url=None, default_ttl_hours=24):
22
+ """
23
+ Initialize Redis job store.
24
+
25
+ Args:
26
+ redis_url (str): Redis connection URL. If None, uses current_app.config['REDIS_URL']
27
+ default_ttl_hours (int): Default time-to-live for jobs in hours
28
+ """
29
+ self.default_ttl_hours = default_ttl_hours
30
+ if redis_url:
31
+ self.redis_client = redis.from_url(redis_url)
32
+ elif current_app and hasattr(current_app, 'config') and 'REDIS_URL' in current_app.config:
33
+ self.redis_client = redis.from_url(current_app.config['REDIS_URL'])
34
+ else:
35
+ # Default to localhost Redis
36
+ self.redis_client = redis.from_url('redis://localhost:6379/0')
37
+
38
+ def _validate_job_id(self, job_id):
39
+ """
40
+ Validate job ID format to prevent injection attacks.
41
+
42
+ Args:
43
+ job_id (str): Job ID to validate
44
+
45
+ Returns:
46
+ bool: True if valid, False otherwise
47
+ """
48
+ if not job_id or not isinstance(job_id, str):
49
+ return False
50
+ # Allow UUID format or alphanumeric with hyphens/underscores
51
+ return bool(re.match(r'^[a-zA-Z0-9_-]{1,64}$', job_id))
52
+
53
+ def _validate_status(self, status):
54
+ """
55
+ Validate job status value.
56
+
57
+ Args:
58
+ status (str): Status to validate
59
+
60
+ Returns:
61
+ bool: True if valid, False otherwise
62
+ """
63
+ return status in self.VALID_STATUSES
64
+
65
+ def create_job(self, job_id=None, initial_status='pending', initial_data=None):
66
+ """
67
+ Create a new job in Redis.
68
+
69
+ Args:
70
+ job_id (str): Job ID. If None, generates a new UUID.
71
+ initial_status (str): Initial job status.
72
+ initial_data (dict): Initial job data.
73
+
74
+ Returns:
75
+ str: Job ID
76
+ """
77
+ if initial_status and not self._validate_status(initial_status):
78
+ raise ValueError(f"Invalid status: {initial_status}. Valid statuses are: {self.VALID_STATUSES}")
79
+
80
+ if job_id and not self._validate_job_id(job_id):
81
+ raise ValueError(f"Invalid job ID format: {job_id}")
82
+
83
+ if job_id is None:
84
+ job_id = str(uuid.uuid4())
85
+
86
+ job_data = {
87
+ 'status': initial_status,
88
+ 'result': None,
89
+ 'error': None,
90
+ 'created_at': datetime.utcnow().isoformat(),
91
+ 'updated_at': datetime.utcnow().isoformat()
92
+ }
93
+
94
+ if initial_data:
95
+ job_data.update(initial_data)
96
+
97
+ try:
98
+ # Store job data as JSON in Redis with expiration (24 hours)
99
+ self.redis_client.setex(
100
+ f"job:{job_id}",
101
+ timedelta(hours=self.default_ttl_hours),
102
+ json.dumps(job_data)
103
+ )
104
+ except redis.ConnectionError:
105
+ logging.error(f"Failed to connect to Redis when creating job {job_id}")
106
+ raise
107
+ except Exception as e:
108
+ logging.error(f"Unexpected error when creating job {job_id}: {str(e)}")
109
+ raise
110
+
111
+ return job_id
112
+
113
+ def get_job(self, job_id):
114
+ """
115
+ Get job data from Redis.
116
+
117
+ Args:
118
+ job_id (str): Job ID
119
+
120
+ Returns:
121
+ dict: Job data or None if not found
122
+ """
123
+ if not self._validate_job_id(job_id):
124
+ raise ValueError(f"Invalid job ID format: {job_id}")
125
+
126
+ try:
127
+ job_data_json = self.redis_client.get(f"job:{job_id}")
128
+ if job_data_json:
129
+ return json.loads(job_data_json)
130
+ return None
131
+ except redis.ConnectionError:
132
+ logging.error(f"Failed to connect to Redis when getting job {job_id}")
133
+ return None
134
+ except json.JSONDecodeError:
135
+ logging.error(f"Failed to decode JSON for job {job_id}")
136
+ return None
137
+ except Exception as e:
138
+ logging.error(f"Unexpected error when getting job {job_id}: {str(e)}")
139
+ return None
140
+
141
+ def update_job(self, job_id, status=None, result=None, error=None):
142
+ """
143
+ Update job status and data in Redis using atomic operations to prevent race conditions.
144
+
145
+ Args:
146
+ job_id (str): Job ID
147
+ status (str): New status
148
+ result (any): Result data
149
+ error (str): Error message
150
+
151
+ Returns:
152
+ bool: True if job was updated, False if not found
153
+ """
154
+ if not self._validate_job_id(job_id):
155
+ raise ValueError(f"Invalid job ID format: {job_id}")
156
+
157
+ if status is not None and not self._validate_status(status):
158
+ raise ValueError(f"Invalid status: {status}. Valid statuses are: {self.VALID_STATUSES}")
159
+
160
+ # Use Lua script for atomic read-modify-write operation
161
+ lua_script = """
162
+ local job_key = KEYS[1]
163
+ local job_data = redis.call('GET', job_key)
164
+
165
+ if not job_data then
166
+ return 0
167
+ end
168
+
169
+ local updated_data = cjson.decode(job_data)
170
+
171
+ if ARGV[1] ~= 'nil' then
172
+ updated_data.status = ARGV[1]
173
+ end
174
+ if ARGV[2] ~= 'nil' then
175
+ updated_data.result = cjson.decode(ARGV[2])
176
+ end
177
+ if ARGV[3] ~= 'nil' then
178
+ updated_data.error = ARGV[3]
179
+ end
180
+
181
+ updated_data.updated_at = ARGV[4]
182
+
183
+ local ttl = redis.call('TTL', job_key)
184
+ redis.call('SET', job_key, cjson.encode(updated_data), 'EX', ttl)
185
+
186
+ return 1
187
+ """
188
+
189
+ try:
190
+ # Prepare arguments for the Lua script
191
+ status_arg = status if status is not None else 'nil'
192
+ result_arg = json.dumps(result) if result is not None else 'nil'
193
+ error_arg = error if error is not None else 'nil'
194
+ updated_at_arg = datetime.utcnow().isoformat()
195
+
196
+ script = self.redis_client.register_script(lua_script)
197
+ result = script(keys=[f"job:{job_id}"],
198
+ args=[status_arg, result_arg, error_arg, updated_at_arg])
199
+
200
+ return result == 1
201
+ except redis.ConnectionError:
202
+ logging.error(f"Failed to connect to Redis when updating job {job_id}")
203
+ return False
204
+ except Exception as e:
205
+ logging.error(f"Unexpected error when updating job {job_id}: {str(e)}")
206
+ return False
207
+
208
+ def delete_job(self, job_id):
209
+ """
210
+ Delete a job from Redis.
211
+
212
+ Args:
213
+ job_id (str): Job ID
214
+
215
+ Returns:
216
+ bool: True if job was deleted, False if not found
217
+ """
218
+ if not self._validate_job_id(job_id):
219
+ raise ValueError(f"Invalid job ID format: {job_id}")
220
+
221
+ try:
222
+ result = self.redis_client.delete(f"job:{job_id}")
223
+ return result > 0
224
+ except redis.ConnectionError:
225
+ logging.error(f"Failed to connect to Redis when deleting job {job_id}")
226
+ return False
227
+ except Exception as e:
228
+ logging.error(f"Unexpected error when deleting job {job_id}: {str(e)}")
229
+ return False
230
+
231
+ def cleanup_expired_jobs(self):
232
+ """
233
+ Clean up jobs that have expired based on their creation time.
234
+ This is a placeholder method - in a real implementation, you might want to
235
+ scan for expired jobs and remove them, but Redis automatically handles TTL.
236
+ """
237
+ # Redis handles TTL automatically, so this is mostly for documentation
238
+ # In a production system, you might want to implement custom cleanup logic
239
+ pass
240
+
241
+
242
+ def get_redis_job_store():
243
+ """
244
+ Get the Redis job store instance from the current app context.
245
+
246
+ Returns:
247
+ RedisJobStore: Redis job store instance
248
+ """
249
+ if not hasattr(current_app, 'redis_job_store'):
250
+ redis_url = current_app.config.get('REDIS_URL', 'redis://localhost:6379/0')
251
+ current_app.redis_job_store = RedisJobStore(redis_url)
252
+
253
+ return current_app.redis_job_store
docs/sprint-artifacts/architecture.md ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin - LinkedIn Community Manager Brownfield Enhancement Architecture
2
+
3
+ ## Change Log
4
+ | Change | Date | Version | Description | Author |
5
+ |--------|------|---------|-------------|---------|
6
+ | Initial Draft | 2025-10-20 | 1.0 | Initial architecture document for UI/UX improvements, keyword analysis, and FLUX.1-dev image generation enhancements | Architect |
7
+
8
+ ## 1. Introduction
9
+
10
+ This document outlines the architectural approach for enhancing Lin with UI/UX improvements, keyword relevance analysis, and upgraded image generation capabilities. Its primary goal is to serve as the guiding architectural blueprint for AI-driven development of new features while ensuring seamless integration with the existing system.
11
+
12
+ **Relationship to Existing Architecture:**
13
+ This document supplements existing project architecture by defining how new components will integrate with current systems. Where conflicts arise between new and existing patterns, this document provides guidance on maintaining consistency while implementing enhancements.
14
+
15
+ ### 1.1 Existing Project Analysis
16
+
17
+ Based on my analysis of your project, I've identified the following about your existing system:
18
+ - The application is a LinkedIn community management tool with React frontend and Flask backend
19
+ - Uses Supabase for authentication and database
20
+ - Has established AI content generation using Gradio client
21
+ - Current image generation uses Qwen/Qwen-Image model
22
+ - Well-structured with clear separation of concerns between frontend and backend
23
+ - Has established API patterns and Redux state management
24
+
25
+ Please confirm these observations are accurate before I proceed with architectural recommendations.
26
+
27
+ #### Current Project State
28
+ - **Primary Purpose:** LinkedIn community management tool with AI-powered content generation
29
+ - **Current Tech Stack:** React (frontend), Flask (backend), Supabase (database/auth), Gradio client (AI integration)
30
+ - **Architecture Style:** Microservices-like with clear separation between frontend and backend
31
+ - **Deployment Method:** Docker with docker-compose, with Nginx reverse proxy
32
+
33
+ #### Available Documentation
34
+ - README.md: Complete project documentation with setup instructions
35
+ - Backend README.md: Detailed backend API documentation
36
+ - Frontend README.md: Frontend development guide
37
+ - docs/prd.md: Product requirements document
38
+
39
+ #### Identified Constraints
40
+ - Must maintain backward compatibility with existing user workflows
41
+ - Authentication system is based on JWT tokens and Supabase
42
+ - Image generation currently uses Qwen model through Gradio client
43
+ - Existing API patterns must be preserved
44
+
45
+ ## 2. Enhancement Scope and Integration Strategy
46
+
47
+ ### 2.1 Enhancement Overview
48
+ **Enhancement Type:** UI/UX Overhaul, New Feature Addition, Integration with New Systems
49
+ **Scope:** UI/UX improvements to the dashboard, keyword relevance analysis feature, replacement of current image generation with FLUX.1-dev
50
+ **Integration Impact:** Medium Impact (requires changes to existing code but maintains compatibility)
51
+
52
+ ### 2.2 Integration Approach
53
+ **Code Integration Strategy:** Follow existing patterns and conventions in the codebase
54
+ **Database Integration:** No schema changes required, leveraging existing tables
55
+ **API Integration:** Extend existing API endpoints while maintaining compatibility
56
+ **UI Integration:** Enhance existing UI components following established design patterns
57
+
58
+ ### 2.3 Compatibility Requirements
59
+ - **Existing API Compatibility:** All new endpoints must follow existing authentication patterns
60
+ - **Database Schema Compatibility:** No schema changes required, using existing tables
61
+ - **UI/UX Consistency:** Follow existing design system and component patterns
62
+ - **Performance Impact:** Maintain current performance characteristics
63
+
64
+ ## 3. Tech Stack
65
+
66
+ ### 3.1 Existing Technology Stack
67
+ | Category | Current Technology | Version | Usage in Enhancement | Notes |
68
+ |----------|-------------------|---------|---------------------|--------|
69
+ | Frontend Framework | React | 18.2.0 | UI components for new features | Continue using existing patterns |
70
+ | Build Tool | Vite | - | Build process for enhanced UI | Continue using existing configuration |
71
+ | State Management | Redux Toolkit | - | State management for new features | Continue using existing patterns |
72
+ | Styling | Tailwind CSS | - | Styling for new components | Follow existing design system |
73
+ | Backend Framework | Flask | 3.1.1 | API endpoints for new features | Extend existing API structure |
74
+ | Database | Supabase (PostgreSQL) | - | Data storage for new features | Use existing tables and auth |
75
+ | Authentication | JWT + Supabase | - | Authentication for new features | Use existing auth patterns |
76
+ | AI Integration | Gradio Client | - | Image generation replacement | Replace Qwen with FLUX.1-dev |
77
+ | Task Queue | Celery + Redis | - | Async processing for image generation | Continue using existing setup |
78
+
79
+ ### 3.2 New Technology Additions
80
+ No new major technologies are being introduced. The enhancement involves replacing the current Qwen image generation with FLUX.1-dev while maintaining all other existing technologies.
81
+
82
+ ## 4. Data Models and Schema Changes
83
+
84
+ ### 4.1 Schema Integration Strategy
85
+ **Database Changes Required:**
86
+ - **New Tables:** None
87
+ - **Modified Tables:** None
88
+ - **New Indexes:** None
89
+ - **Migration Strategy:** None required
90
+
91
+ **Backward Compatibility:**
92
+ - No changes to existing data models
93
+ - All existing functionality remains intact
94
+ - New features use existing database structure
95
+
96
+ ## 5. Component Architecture
97
+
98
+ ### 5.1 New Components
99
+
100
+ #### KeywordAnalysisService
101
+ **Responsibility:** Handle keyword frequency analysis for content planning
102
+ **Integration Points:** Integrated with existing content service and API endpoints
103
+
104
+ **Key Interfaces:**
105
+ - analyze_keyword_frequency(keywords: List[str]) -> Dict[str, str]
106
+
107
+ **Dependencies:**
108
+ - **Existing Components:** Uses existing database connection and authentication
109
+ - **New Components:** None
110
+
111
+ **Technology Stack:** Python, existing Flask framework
112
+
113
+ #### ImageGenerationService (Updated)
114
+ **Responsibility:** Handle image generation using FLUX.1-dev instead of Qwen
115
+ **Integration Points:** Integrated with existing content service and AI workflow
116
+
117
+ **Key Interfaces:**
118
+ - generate_flux_image(prompt: str, seed: int, dimensions: tuple, guidance_scale: float, inference_steps: int) -> str
119
+
120
+ **Dependencies:**
121
+ - **Existing Components:** Uses existing gradio_client and authentication
122
+ - **New Components:** None
123
+
124
+ **Technology Stack:** Python, gradio_client, existing Flask framework
125
+
126
+ ### 5.2 Component Interaction Diagram
127
+ ```mermaid
128
+ graph TB
129
+ subgraph "Frontend"
130
+ A[Posts Page] --> B[KeywordAnalysisPanel]
131
+ A --> C[ImageGenerationPanel]
132
+ B --> D[KeywordAnalysisService]
133
+ C --> E[ImageGenerationService]
134
+ end
135
+
136
+ subgraph "Backend API"
137
+ F[app.py] --> G[posts_bp]
138
+ G --> H[content_service]
139
+ G --> I[keyword_analysis_service]
140
+ end
141
+
142
+ subgraph "AI Services"
143
+ H --> J[FLUX.1-dev via gradio_client]
144
+ I --> K[Existing RSS/Post Data]
145
+ end
146
+
147
+ subgraph "Database"
148
+ L[Supabase] --> H
149
+ L --> I
150
+ end
151
+
152
+ D -.-> G
153
+ E -.-> G
154
+ B -.-> D
155
+ C -.-> E
156
+ ```
157
+
158
+ ## 6. API Design and Integration
159
+
160
+ ### 6.1 API Integration Strategy
161
+ **API Integration Strategy:** Extend existing `/api/posts` endpoints while maintaining compatibility
162
+ **Authentication:** Use existing JWT token authentication
163
+ **Versioning:** No versioning needed, following existing API patterns
164
+
165
+ ### 6.2 New API Endpoints
166
+
167
+ #### POST /api/posts/keyword-analysis
168
+ **Method:** POST
169
+ **Endpoint:** /api/posts/keyword-analysis
170
+ **Purpose:** Analyze keyword frequency and relevance
171
+ **Integration:** With existing posts API and authentication
172
+
173
+ **Request:**
174
+ ```json
175
+ {
176
+ "keywords": ["keyword1", "keyword2"]
177
+ }
178
+ ```
179
+
180
+ **Response:**
181
+ ```json
182
+ {
183
+ "results": {
184
+ "keyword1": "daily",
185
+ "keyword2": "weekly"
186
+ },
187
+ "status": "success"
188
+ }
189
+ ```
190
+
191
+ ## 7. External API Integration
192
+
193
+ ### 7.1 FLUX.1-dev API
194
+ **Purpose:** High-quality image generation to replace current Qwen implementation
195
+ **Documentation:** Available through Hugging Face Spaces
196
+ **Base URL:** Hugging Face Space for FLUX.1-dev
197
+ **Authentication:** Using existing HUGGING_KEY environment variable
198
+
199
+ **Key Endpoints Used:**
200
+ - `POST /infer` - Image generation with parameters
201
+
202
+ **Error Handling:** Fallback to existing functionality if FLUX.1-dev fails
203
+
204
+ ## 8. Source Tree
205
+
206
+ ### 8.1 Existing Project Structure
207
+ ```
208
+ Lin/
209
+ ├── .env.hf
210
+ ├── .gitattributes
211
+ ├── .gitignore
212
+ ├── .kilocodemodes
213
+ ├── app.py
214
+ ├── docker-compose.yml
215
+ ├── Dockerfile
216
+ ├── nginx.conf
217
+ ├── package-lock.json
218
+ ├── package.json
219
+ ├── README.md
220
+ ├── requirements.txt
221
+ ├── SETUP_GUIDE.md
222
+ ├── simple_timezone_test.py
223
+ ├── start_app.py
224
+ ├── start_celery.py
225
+ ├── start-dev.js
226
+ ├── starty.py
227
+ ├── test_apscheduler.py
228
+ ├── test_imports.py
229
+ ├── test_scheduler_integration.py
230
+ ├── test_scheduler_visibility.py
231
+ ├── test_timezone_functionality.py
232
+ ├── .qwen/
233
+ ├── backend/
234
+ │ ├── __init__.py
235
+ │ ├── .env.example
236
+ │ ├── app.py
237
+ │ ├── config.py
238
+ │ ├── Dockerfile
239
+ │ ├── README.md
240
+ │ ├── requirements.txt
241
+ │ ├── test_database_connection.py
242
+ │ ├── test_oauth_callback.py
243
+ │ ├── test_oauth_flow.py
244
+ │ ├── TESTING_GUIDE.md
245
+ │ ├── api/
246
+ │ │ ├── __init__.py
247
+ │ │ ├── accounts.py
248
+ │ │ ├── auth.py
249
+ │ │ ├── posts.py
250
+ │ │ ├── schedules.py
251
+ │ │ └── sources.py
252
+ │ ├── models/
253
+ │ │ ├── __init__.py
254
+ │ │ ├── schedule.py
255
+ │ │ └── user.py
256
+ │ ├── scheduler/
257
+ │ │ ├── __init__.py
258
+ │ │ └── apscheduler_service.py
259
+ │ ├── services/
260
+ │ │ ├── __init__.py
261
+ │ │ ├── auth_service.py
262
+ �� │ ├── content_service.py
263
+ │ │ ├── linkedin_service.py
264
+ │ │ └── schedule_service.py
265
+ │ ├── tests/
266
+ │ │ ├── test_frontend_integration.py
267
+ │ │ └── test_scheduler_image_integration.py
268
+ │ ├── utils/
269
+ │ │ ├── __init__.py
270
+ │ │ ├── cookies.py
271
+ │ │ ├── database.py
272
+ │ │ ├── image_utils.py
273
+ │ │ └── timezone_utils.py
274
+ │ └── .gitignore
275
+ ├── docu_code/
276
+ │ ├── My_data_base_schema_.txt
277
+ │ └── supabase.txt
278
+ ├── fav/
279
+ │ └── Capture d'écran 2025-08-16 223532.png
280
+ ├── frontend/
281
+ │ ├── .env.development
282
+ │ ├── .env.example
283
+ │ ├── .env.production
284
+ │ ├── .eslintrc.cjs
285
+ │ ├── DESIGN_SYSTEM.md
286
+ │ ├── Dockerfile
287
+ │ ├── index.html
288
+ │ ├── package-lock.json
289
+ │ ├── package.json
290
+ │ ├── postcss.config.js
291
+ │ ├── README.md
292
+ │ ├── RESPONSIVE_DESIGN_VALIDATION.md
293
+ │ ├── tailwind.config.js
294
+ │ ├── test-auth-fix.js
295
+ │ ├── tsconfig.json
296
+ │ ├── tsconfig.node.json
297
+ │ ├── vite.config.js
298
+ │ ├── public/
299
+ │ │ ├── favicon.ico
300
+ │ │ ├── favicon.png
301
+ │ │ ├── index.html
302
+ │ │ └── manifest.json
303
+ │ ├── scripts/
304
+ │ │ └── build-env.js
305
+ │ ├── src/
306
+ │ │ ├── App.css
307
+ │ │ ├── App.jsx
308
+ │ │ ├── index.css
309
+ │ │ ├── index.jsx
310
+ │ │ ├── layout-test.js
311
+ │ │ ├── responsive-design-test.js
312
+ │ │ ├── responsive.css
313
+ │ │ ├── components/
314
+ │ │ │ ├── FeatureCard.jsx
315
+ │ │ │ ├── TestimonialCard.jsx
316
+ │ │ │ ├── Header/
317
+ │ │ │ │ ├── Header.css
318
+ │ │ │ │ └── Header.jsx
319
+ │ │ │ ├── LinkedInAccount/
320
+ │ │ │ │ ├── LinkedInAccountCard.jsx
321
+ │ │ │ │ ├── LinkedInAccountsManager.jsx
322
+ │ │ │ │ └── LinkedInCallbackHandler.jsx
323
+ │ │ │ └── Sidebar/
324
+ │ │ │ └── Sidebar.jsx
325
+ │ │ ├── css/
326
+ │ │ │ ├── base.css
327
+ │ │ │ ├── components.css.bak
328
+ │ │ │ ├── main.css
329
+ │ │ │ ├── responsive.css
330
+ │ │ │ ├── typography.css
331
+ │ │ │ ├── variables.css
332
+ │ │ │ ├── components/
333
+ │ │ │ ├── buttons.css
334
+ │ │ │ │ ├── cards.css
335
+ │ │ │ │ ├── forms.css
336
+ │ │ │ │ ├── grid.css
337
+ │ │ │ │ ├── header.css
338
+ │ │ │ │ ├── linkedin.css
339
+ │ │ │ │ ├── modal.css
340
+ │ │ │ │ ├── navigation.css
341
+ │ │ │ │ ├── sidebar.css
342
+ │ │ │ │ ├── table.css
343
+ │ │ │ │ └── utilities.css
344
+ │ │ │ └── responsive/
345
+ │ │ │ ├── accessibility.css
346
+ │ │ │ ├── base.css
347
+ │ │ │ ├── mobile-nav.css
348
+ │ │ │ ├── performance.css
349
+ │ │ │ └── performance/
350
+ │ │ │ ├── lazy-loading.css
351
+ │ │ │ └── mobile-optimization.css
352
+ │ │ ├── debug/
353
+ │ │ │ ├── testApi.js
354
+ │ │ │ └── testApiIntegration.js
355
+ │ │ ├── pages/
356
+ │ │ │ ├── Accounts.jsx
357
+ │ │ │ ├── Dashboard.jsx
358
+ │ │ │ ├── ForgotPassword.jsx
359
+ │ │ │ ├── Home.jsx
360
+ │ │ │ ├── Login.jsx
361
+ │ │ │ ├── Posts.jsx
362
+ │ │ │ ├── Register.jsx
363
+ │ │ │ ├── ResetPassword.jsx
364
+ │ │ │ ├── Schedule.jsx
365
+ │ │ │ └── Sources.jsx
366
+ │ │ ├── services/
367
+ │ │ │ ├── accountService.js
368
+ │ │ │ ├── api.js
369
+ │ │ │ ├── apiClient.js
370
+ │ │ │ ├── authService.js
371
+ │ │ │ ├── cacheService.js
372
+ │ │ │ ├── cookieService.js
373
+ │ │ │ ├── linkedinAuthService.js
374
+ │ │ │ ├── postService.js
375
+ │ │ │ ├── scheduleService.js
376
+ │ │ │ ├── securityService.js
377
+ │ │ │ ├── sourceService.js
378
+ │ │ │ └── supabaseClient.js
379
+ │ │ ├── store/
380
+ │ │ │ ├── index.js
381
+ │ │ │ └── reducers/
382
+ │ │ │ ├── accountsSlice.js
383
+ │ │ │ ├── authSlice.js
384
+ │ │ │ ├── linkedinAccountsSlice.js
385
+ │ │ │ ├── postsSlice.js
386
+ │ │ │ ├── schedulesSlice.js
387
+ │ │ │ └── sourcesSlice.js
388
+ │ │ └─�� utils/
389
+ │ │ └── timezoneUtils.js
390
+ │ └── .gitignore
391
+ ├── Linkedin_poster_dev/
392
+ │ ├── .gitattributes
393
+ │ ├── ai_agent.py
394
+ │ ├── app.py
395
+ │ ├── README.md
396
+ │ └── requirements.txt
397
+ └── docs/
398
+ └── architecture.md
399
+ ```
400
+
401
+ ### 8.2 New File Organization
402
+ ```
403
+ Lin/
404
+ ├── frontend/
405
+ │ └── src/
406
+ │ ├── components/
407
+ │ │ └── KeywordAnalysis/ # New keyword analysis components
408
+ │ │ ├── KeywordAnalysisPanel.jsx
409
+ │ │ └── index.js
410
+ │ └── services/
411
+ │ └── keywordAnalysisService.js
412
+ ├── backend/
413
+ │ ├── services/
414
+ │ │ ├── keyword_analysis_service.py # New service
415
+ │ │ └── content_service.py # Updated with FLUX.1-dev
416
+ │ └── api/
417
+ │ └── posts.py # Extended with new endpoints
418
+ └── Linkedin_poster_dev/
419
+ └── ai_agent.py # Updated with FLUX.1-dev
420
+ ```
421
+
422
+ ### 8.3 Integration Guidelines
423
+ - **File Naming:** Follow existing snake_case for Python and camelCase for JavaScript
424
+ - **Folder Organization:** Place new components in appropriate existing directories
425
+ - **Import/Export Patterns:** Maintain existing patterns in the codebase
426
+
427
+ ## 9. Infrastructure and Deployment Integration
428
+
429
+ ### 9.1 Existing Infrastructure
430
+ **Current Deployment:** Docker with docker-compose and Nginx reverse proxy
431
+ **Infrastructure Tools:** Docker, docker-compose, Nginx, Redis for Celery
432
+ **Environments:** Development and production configurations available
433
+
434
+ ### 9.2 Enhancement Deployment Strategy
435
+ **Deployment Approach:** No infrastructure changes required, using existing setup
436
+ **Infrastructure Changes:** None
437
+ **Pipeline Integration:** No changes to existing deployment pipeline
438
+
439
+ ### 9.3 Rollback Strategy
440
+ **Rollback Method:** Revert changes to ai_agent.py to restore Qwen functionality
441
+ **Risk Mitigation:** Thorough testing before deployment
442
+ **Monitoring:** Monitor API response times and error rates
443
+
444
+ ## 10. Coding Standards
445
+
446
+ ### 10.1 Existing Standards Compliance
447
+ **Code Style:** Follow existing Python (PEP 8) and JavaScript (ESLint) standards
448
+ **Linting Rules:** Use existing linting configurations
449
+ **Testing Patterns:** Follow existing pytest and React testing patterns
450
+ **Documentation Style:** Follow existing docstring and JSDoc patterns
451
+
452
+ ### 10.2 Critical Integration Rules
453
+ - **Existing API Compatibility:** New endpoints must follow existing authentication patterns
454
+ - **Database Integration:** Use existing Supabase connection and query patterns
455
+ - **Error Handling:** Follow existing error response format
456
+ - **Logging Consistency:** Use existing logging patterns
457
+
458
+ ## 11. Testing Strategy
459
+
460
+ ### 11.1 Integration with Existing Tests
461
+ **Existing Test Framework:** pytest for backend, Jest/React Testing Library for frontend
462
+ **Test Organization:** Follow existing test directory structure
463
+ **Coverage Requirements:** Maintain existing coverage thresholds
464
+
465
+ ### 11.2 New Testing Requirements
466
+
467
+ #### Unit Tests for New Components
468
+ **Framework:** pytest for backend, React Testing Library for frontend
469
+ **Location:** backend/tests/ and frontend/src/tests/
470
+ **Coverage Target:** 80%+ for new code
471
+ **Integration with Existing:** Follow existing test patterns
472
+
473
+ #### Integration Tests
474
+ **Scope:** Test new API endpoints with authentication
475
+ **Existing System Verification:** Ensure existing functionality remains intact
476
+ **New Feature Testing:** Validate keyword analysis and image generation
477
+
478
+ #### Regression Testing
479
+ **Existing Feature Verification:** Run all existing tests to ensure no regressions
480
+ **Automated Regression Suite:** Use existing CI pipeline
481
+ **Manual Testing Requirements:** Test end-to-end workflows manually
482
+
483
+ ## 12. Security Integration
484
+
485
+ ### 12.1 Existing Security Measures
486
+ **Authentication:** JWT token-based authentication
487
+ **Authorization:** Role-based access control
488
+ **Data Protection:** Supabase security and encryption
489
+ **Security Tools:** Built-in Flask security features
490
+
491
+ ### 12.2 Enhancement Security Requirements
492
+ **New Security Measures:** Input validation for new API endpoints
493
+ **Integration Points:** Use existing authentication for all new endpoints
494
+ **Compliance Requirements:** Maintain existing data privacy standards
495
+
496
+ ### 12.3 Security Testing
497
+ **Existing Security Tests:** Continue running existing security tests
498
+ **New Security Test Requirements:** Validate input sanitization for new endpoints
499
+ **Penetration Testing:** None specifically required for these enhancements
500
+
501
+ ## 13. Next Steps
502
+
503
+ ### 13.1 Story Manager Handoff
504
+ The architecture document provides a clear roadmap for implementing the UI/UX improvements, keyword analysis feature, and FLUX.1-dev image generation. The key integration requirements have been validated with the existing system. Begin with implementing the keyword analysis feature, followed by the FLUX.1-dev integration, and finally the UI/UX enhancements. Emphasis should be placed on maintaining existing system integrity throughout implementation.
505
+
506
+ ### 13.2 Developer Handoff
507
+ Developers should reference this architecture document and existing coding standards when starting implementation. The integration requirements with the existing codebase have been validated. Key technical decisions are based on real project constraints, and existing system compatibility requirements include specific verification steps for API compatibility. The implementation should follow a clear sequence to minimize risk to existing functionality: keyword analysis service first, then FLUX.1-dev integration, and finally UI enhancements.
docs/sprint-artifacts/tech_spec_job_tracking.md ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tech-Spec 1: Fix Background Task and Job Tracking in Gunicorn Environment
2
+
3
+ **Created:** 2025-12-22
4
+ **Status:** Completed
5
+
6
+ ## Overview
7
+
8
+ ### Problem Statement
9
+ After migrating from Flask's development server to Gunicorn, the background task system for post generation is not working correctly. While tasks are submitted and processed successfully on the generation server, the job status tracking fails because job states are not accessible across multiple Gunicorn workers. This results in 404 errors when trying to fetch job status and causes the SSE connection to time out, preventing updates from reaching the frontend.
10
+
11
+ ### Solution
12
+ Implemented a shared storage mechanism for job state management that works across multiple Gunicorn workers, ensuring consistent job tracking and proper SSE communication.
13
+
14
+ ### Scope (In/Out)
15
+
16
+ **In Scope:**
17
+ - Implement shared job state storage (Redis, database, or other shared storage)
18
+ - Update background task management to use shared storage instead of in-memory storage
19
+ - Fix SSE connection handling in multi-worker environment using Flask's streaming features
20
+ - Update job status polling endpoints to access shared storage
21
+ - Ensure proper request context handling for streaming responses
22
+
23
+ **Out Scope:**
24
+ - Changing the Gradio API integration
25
+ - Modifying the frontend UI components
26
+ - Refactoring the core post generation logic
27
+
28
+ ## Context for Development
29
+
30
+ ### Codebase Patterns
31
+ - Flask REST API backend
32
+ - Background task processing with job tracking
33
+ - Server-sent events for real-time updates
34
+ - Gradio API integration for content generation
35
+ - Use of `stream_with_context` for streaming responses
36
+
37
+ ### Files to Reference
38
+ - `app.py` - Main Flask application
39
+ - `posts.py` - Contains the job submission and tracking logic (around line 196 based on logs)
40
+ - `content_service.py` - Gradio API integration
41
+ - `start_gunicorn.py` - Gunicorn configuration
42
+ - Frontend JavaScript files handling SSE connections
43
+
44
+ ### Technical Decisions
45
+ - Use Redis for shared job state storage (fast, simple, good for temporary job data)
46
+ - Implement proper job lifecycle management (created, processing, completed, failed)
47
+ - Use Flask's `stream_with_context` for proper SSE handling
48
+
49
+ ## Implementation Plan
50
+
51
+ ### Tasks
52
+
53
+ - [x] Task 1: Set up Redis connection and configure Flask-Session with Redis backend
54
+ - [x] Task 2: Update job creation and tracking to use Redis instead of in-memory storage
55
+ - [x] Task 3: Modify background task handlers to update job state in Redis
56
+ - [x] Task 4: Update job status polling endpoint to fetch from Redis
57
+ - [x] Task 5: Fix SSE connection handling using Flask's streaming capabilities with `stream_with_context`
58
+ - [x] Task 6: Test the complete flow from job submission to completion
59
+
60
+ ### Acceptance Criteria
61
+
62
+ - [x] AC 1: Job state is accessible across all Gunicorn workers via Redis
63
+ - [x] AC 2: Job status polling endpoint returns correct job status (not 404)
64
+ - [x] AC 3: SSE connections receive real-time updates about job progress using proper streaming
65
+ - [x] AC 4: Generated posts are properly displayed on the frontend after completion
66
+ - [x] AC 5: No regressions in existing functionality
67
+
68
+ ## Additional Context
69
+
70
+ ### Dependencies
71
+ - Redis server for shared storage
72
+ - Updated Flask application with Redis integration
73
+ - Flask-Session with Redis backend
74
+ - Gunicorn configuration with appropriate worker settings
75
+
76
+ ### Testing Strategy
77
+ - Unit tests for Redis job state management
78
+ - Integration tests for job submission and status tracking
79
+ - End-to-end test of the complete post generation flow
80
+ - Test SSE connections with multiple Gunicorn workers
81
+
82
+ ### Notes
83
+ - Consider job cleanup for completed/failed jobs to prevent Redis memory issues
84
+ - Ensure proper error handling when Redis is unavailable
85
+ - May need to adjust Gunicorn worker count to optimize for background tasks
86
+ - Use Flask's `stream_with_context` to properly handle streaming responses in multi-worker environments
docs/sprint-artifacts/tech_spec_remote_logging.md ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tech-Spec 2: Remote Application Logging with Redis
2
+
3
+ **Created:** 2025-12-22
4
+ **Status:** Ready for Development
5
+
6
+ ## Overview
7
+
8
+ ### Problem Statement
9
+ Need to centralize application logs in Redis to enable remote access and monitoring of application behavior across multiple Gunicorn workers. This will help with debugging issues like the background task tracking problem and provide better visibility into application performance.
10
+
11
+ ### Solution
12
+ Implement a logging system that sends application logs to Redis, enabling remote access, centralized monitoring, and real-time log streaming.
13
+
14
+ ### Scope (In/Out)
15
+
16
+ **In Scope:**
17
+ - Implement Redis-based logging handler
18
+ - Configure log formatting for Redis storage
19
+ - Create log retrieval endpoints
20
+ - Implement log streaming capabilities
21
+ - Add log retention and cleanup policies
22
+
23
+ **Out Scope:**
24
+ - Replacing existing logging infrastructure completely
25
+ - Implementing complex log analytics
26
+ - Creating a full dashboard UI
27
+
28
+ ## Context for Development
29
+
30
+ ### Codebase Patterns
31
+ - Flask application with existing logging infrastructure
32
+ - Multiple Gunicorn workers requiring centralized logging
33
+ - Need for remote log access and debugging
34
+
35
+ ### Files to Reference
36
+ - `app.py` - Main Flask application where logging is configured
37
+ - `gunicorn.conf.py` - Gunicorn configuration
38
+ - Any existing logging configuration files
39
+
40
+ ### Technical Decisions
41
+ - Use Redis lists for log storage with push operations
42
+ - Implement log level filtering and retention
43
+ - Use structured logging format for easier parsing
44
+ - Add log rotation to prevent Redis memory issues
45
+
46
+ ## Implementation Plan
47
+
48
+ ### Tasks
49
+
50
+ - [ ] Task 1: Set up Redis connection for logging
51
+ - [ ] Task 2: Create custom Python logging handler for Redis
52
+ - [ ] Task 3: Configure Flask application to use Redis logger
53
+ - [ ] Task 4: Create API endpoints to retrieve logs from Redis
54
+ - [ ] Task 5: Implement log retention and cleanup policies
55
+ - [ ] Task 6: Test logging functionality with multiple Gunicorn workers
56
+
57
+ ### Acceptance Criteria
58
+
59
+ - [ ] AC 1: Application logs are stored in Redis with proper formatting
60
+ - [ ] AC 2: Remote access to logs via API endpoints
61
+ - [ ] AC 3: Logs from all Gunicorn workers are consolidated in Redis
62
+ - [ ] AC 4: Log retention prevents Redis memory overflow
63
+ - [ ] AC 5: Structured logs include timestamp, level, module, and message
64
+
65
+ ## Additional Context
66
+
67
+ ### Implementation Details
68
+
69
+ #### 1. Redis Logging Handler
70
+ ```python
71
+ import redis
72
+ import json
73
+ import logging
74
+ from datetime import datetime
75
+
76
+ class RedisLogHandler(logging.Handler):
77
+ def __init__(self, redis_client, key='app_logs', max_logs=1000):
78
+ super().__init__()
79
+ self.redis_client = redis_client
80
+ self.key = key
81
+ self.max_logs = max_logs
82
+
83
+ def emit(self, record):
84
+ try:
85
+ log_entry = {
86
+ 'timestamp': datetime.utcnow().isoformat(),
87
+ 'level': record.levelname,
88
+ 'module': record.module,
89
+ 'function': record.funcName,
90
+ 'line': record.lineno,
91
+ 'message': record.getMessage(),
92
+ 'logger': record.name
93
+ }
94
+ # Add exception info if present
95
+ if record.exc_info:
96
+ log_entry['exception'] = self.format(record)
97
+
98
+ # Push to Redis list
99
+ self.redis_client.lpush(self.key, json.dumps(log_entry))
100
+ # Trim to max_logs to prevent memory issues
101
+ self.redis_client.ltrim(self.key, 0, self.max_logs - 1)
102
+ except Exception:
103
+ self.handleError(record)
104
+ ```
105
+
106
+ #### 2. Flask Integration
107
+ ```python
108
+ import redis
109
+ from flask import Flask
110
+
111
+ # Initialize Redis connection
112
+ redis_client = redis.Redis(host='localhost', port=6379, db=0)
113
+
114
+ # Configure logging
115
+ app = Flask(__name__)
116
+ redis_handler = RedisLogHandler(redis_client, key='flask_app_logs')
117
+ redis_handler.setLevel(logging.INFO)
118
+
119
+ # Add to Flask app logger
120
+ app.logger.addHandler(redis_handler)
121
+ ```
122
+
123
+ #### 3. API Endpoints for Log Retrieval
124
+ ```python
125
+ from flask import jsonify
126
+
127
+ @app.route('/api/logs')
128
+ def get_logs():
129
+ count = request.args.get('count', 50, type=int)
130
+ logs = redis_client.lrange('flask_app_logs', 0, count - 1)
131
+ return jsonify([json.loads(log) for log in logs])
132
+
133
+ @app.route('/api/logs/levels/<level>')
134
+ def get_logs_by_level(level):
135
+ all_logs = redis_client.lrange('flask_app_logs', 0, -1)
136
+ filtered_logs = []
137
+ for log in all_logs:
138
+ log_data = json.loads(log)
139
+ if log_data['level'] == level.upper():
140
+ filtered_logs.append(log_data)
141
+ return jsonify(filtered_logs)
142
+ ```
143
+
144
+ ### Dependencies
145
+ - `redis` - Python Redis client
146
+ - `flask` - For API endpoints
147
+
148
+ ### Testing Strategy
149
+ - Unit test the Redis logging handler
150
+ - Integration test log storage and retrieval
151
+ - Test with multiple Gunicorn workers to ensure logs are centralized
152
+ - Test log retention policies
153
+
154
+ ### Notes
155
+ - Consider using Redis streams instead of lists for more advanced log querying
156
+ - Implement log rotation to prevent Redis memory issues
157
+ - Add authentication/authorization for log access endpoints
158
+ - Consider using a separate Redis database for logs
159
+ - Monitor Redis memory usage with logging enabled
test_redis_job_store.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script to verify Redis job store functionality
3
+ """
4
+ import sys
5
+ import os
6
+
7
+ # Add the project root to the Python path
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from backend.utils.redis_job_store import RedisJobStore
11
+
12
+ def test_redis_job_store():
13
+ """Test the Redis job store functionality."""
14
+ print("Testing Redis Job Store...")
15
+
16
+ # Initialize Redis job store
17
+ try:
18
+ redis_job_store = RedisJobStore('redis://localhost:6379/0')
19
+ print("[OK] Redis job store initialized successfully")
20
+ except Exception as e:
21
+ print(f"[ERROR] Failed to initialize Redis job store: {e}")
22
+ return False
23
+
24
+ # Test creating a job with default status
25
+ try:
26
+ job_id = redis_job_store.create_job(initial_status='pending', initial_data={'test': True})
27
+ print(f"[OK] Job created successfully with ID: {job_id}")
28
+ except Exception as e:
29
+ print(f"[ERROR] Failed to create job: {e}")
30
+ return False
31
+
32
+ # Test getting the job
33
+ try:
34
+ job = redis_job_store.get_job(job_id)
35
+ if job and job['status'] == 'pending':
36
+ print(f"[OK] Job retrieved successfully: {job}")
37
+ else:
38
+ print(f"[ERROR] Job not found or incorrect status: {job}")
39
+ return False
40
+ except Exception as e:
41
+ print(f"[ERROR] Failed to get job: {e}")
42
+ return False
43
+
44
+ # Test updating the job
45
+ try:
46
+ success = redis_job_store.update_job(job_id, status='completed', result={'data': 'test result'})
47
+ if success:
48
+ print("[OK] Job updated successfully")
49
+ else:
50
+ print("[ERROR] Job update failed")
51
+ return False
52
+ except Exception as e:
53
+ print(f"[ERROR] Failed to update job: {e}")
54
+ return False
55
+
56
+ # Test getting the updated job
57
+ try:
58
+ job = redis_job_store.get_job(job_id)
59
+ if job and job['status'] == 'completed' and job['result']['data'] == 'test result':
60
+ print(f"[OK] Updated job retrieved successfully: {job}")
61
+ else:
62
+ print(f"[ERROR] Job not updated correctly: {job}")
63
+ return False
64
+ except Exception as e:
65
+ print(f"[ERROR] Failed to get updated job: {e}")
66
+ return False
67
+
68
+ # Test deleting the job
69
+ try:
70
+ success = redis_job_store.delete_job(job_id)
71
+ if success:
72
+ print("[OK] Job deleted successfully")
73
+ else:
74
+ print("[ERROR] Job deletion failed")
75
+ return False
76
+ except Exception as e:
77
+ print(f"[ERROR] Failed to delete job: {e}")
78
+ return False
79
+
80
+ # Test validation functionality
81
+ try:
82
+ # Test invalid status
83
+ try:
84
+ redis_job_store.create_job(initial_status='invalid_status')
85
+ print("[ERROR] Should have failed with invalid status")
86
+ return False
87
+ except ValueError:
88
+ print("[OK] Correctly rejected invalid status")
89
+
90
+ # Test invalid job ID format
91
+ try:
92
+ redis_job_store.get_job("invalid job id with spaces")
93
+ print("[ERROR] Should have failed with invalid job ID format")
94
+ return False
95
+ except ValueError:
96
+ print("[OK] Correctly rejected invalid job ID format")
97
+ except Exception as e:
98
+ print(f"[ERROR] Validation tests failed: {e}")
99
+ return False
100
+
101
+ print("[OK] All Redis job store tests passed!")
102
+ return True
103
+
104
+ if __name__ == "__main__":
105
+ success = test_redis_job_store()
106
+ if success:
107
+ print("\nRedis job store is working correctly!")
108
+ else:
109
+ print("\nRedis job store tests failed!")
110
+ sys.exit(1)