techprotrade commited on
Commit
862d728
·
verified ·
1 Parent(s): 8408343

Full ATOM backend sync from D:/Annator/ATOM/atom/backend + HF slim runtime (part 3)

Browse files
DEPLOY_STAMP.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 2026-07-30T05:50:20Z backend sync prepared
backend/token_status.txt CHANGED
Binary files a/backend/token_status.txt and b/backend/token_status.txt differ
 
backend/workers/__init__.py ADDED
File without changes
backend/workers/activity_state_worker.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Activity State Worker
3
+
4
+ Background worker that processes user activity state transitions.
5
+ Runs every 60 seconds to check for inactive users and update their states.
6
+
7
+ State Transitions:
8
+ - online → away after 5 minutes of inactivity
9
+ - away → offline after 15 minutes of inactivity
10
+ - offline → online on activity resumption
11
+ """
12
+
13
+ import asyncio
14
+ import logging
15
+ from datetime import datetime
16
+
17
+ from core.database import get_db
18
+ from core.user_activity_service import UserActivityService
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ActivityStateWorker:
24
+ """
25
+ Background worker for processing user activity state transitions.
26
+
27
+ Runs every 60 seconds to:
28
+ 1. Check user state transitions based on inactivity
29
+ 2. Process manual override expiry
30
+ 3. Clean up stale sessions
31
+ 4. Emit state change events (optional, for WebSocket notifications)
32
+
33
+ Performance target: <1s per batch of 100 users
34
+ """
35
+
36
+ def __init__(self, interval_seconds: int = 60):
37
+ self.interval_seconds = interval_seconds
38
+ self.running = False
39
+
40
+ async def run(self):
41
+ """Main worker loop."""
42
+ self.running = True
43
+ logger.info("ActivityStateWorker started")
44
+
45
+ while self.running:
46
+ try:
47
+ start_time = datetime.now()
48
+
49
+ # Process state transitions
50
+ await self.process_state_transitions()
51
+
52
+ # Process manual override expiry
53
+ await self.process_manual_override_expiry()
54
+
55
+ # Cleanup stale sessions
56
+ await self.cleanup_stale_sessions()
57
+
58
+ elapsed = (datetime.now() - start_time).total_seconds()
59
+ logger.info(
60
+ f"ActivityStateWorker cycle completed in {elapsed:.2f}s"
61
+ )
62
+
63
+ except Exception as e:
64
+ logger.error(f"ActivityStateWorker error: {e}", exc_info=True)
65
+
66
+ # Wait for next cycle
67
+ await asyncio.sleep(self.interval_seconds)
68
+
69
+ async def stop(self):
70
+ """Stop the worker."""
71
+ self.running = False
72
+ logger.info("ActivityStateWorker stopped")
73
+
74
+ async def process_state_transitions(self):
75
+ """Process state transitions for inactive users."""
76
+ db = next(get_db())
77
+
78
+ try:
79
+ service = UserActivityService(db)
80
+ transitions = await service.transition_state_batch(limit=100)
81
+
82
+ if transitions["total_processed"] > 0:
83
+ transition_summary = ", ".join([
84
+ f"{k}: {v}"
85
+ for k, v in transitions.items()
86
+ if k != "total_processed" and v > 0
87
+ ])
88
+ logger.info(
89
+ f"State transitions: {transitions['total_processed']} processed"
90
+ + (f" ({transition_summary})" if transition_summary else "")
91
+ )
92
+
93
+ finally:
94
+ db.close()
95
+
96
+ async def process_manual_override_expiry(self):
97
+ """Process manual overrides that have expired."""
98
+ from core.models import UserActivity
99
+ from sqlalchemy.orm import Session
100
+
101
+ db: Session = next(get_db())
102
+
103
+ try:
104
+ # Find expired manual overrides
105
+ now = datetime.utcnow()
106
+ expired = db.query(UserActivity).filter(
107
+ UserActivity.manual_override == True,
108
+ UserActivity.manual_override_expires_at.isnot(None),
109
+ UserActivity.manual_override_expires_at < now
110
+ ).all()
111
+
112
+ count = 0
113
+ for activity in expired:
114
+ # Clear manual override
115
+ activity.manual_override = False
116
+ activity.manual_override_expires_at = None
117
+
118
+ # Recalculate state based on actual activity
119
+ service = UserActivityService(db)
120
+ await service._recalculate_activity_state(activity)
121
+
122
+ count += 1
123
+ logger.info(
124
+ f"Cleared expired manual override for user {activity.user_id}"
125
+ )
126
+
127
+ if count > 0:
128
+ db.commit()
129
+ logger.info(f"Cleared {count} expired manual overrides")
130
+
131
+ finally:
132
+ db.close()
133
+
134
+ async def cleanup_stale_sessions(self):
135
+ """Clean up stale sessions (no heartbeat for >1 hour)."""
136
+ db = next(get_db())
137
+
138
+ try:
139
+ service = UserActivityService(db)
140
+ count = await service.cleanup_stale_sessions(limit=50)
141
+
142
+ if count > 0:
143
+ logger.info(f"Cleaned up {count} stale sessions")
144
+
145
+ finally:
146
+ db.close()
147
+
148
+
149
+ # ============================================================================
150
+ # Worker Entry Point
151
+ # ============================================================================
152
+
153
+ async def main():
154
+ """Entry point for running the worker."""
155
+ worker = ActivityStateWorker(interval_seconds=60)
156
+
157
+ try:
158
+ await worker.run()
159
+ except KeyboardInterrupt:
160
+ logger.info("Received keyboard interrupt")
161
+ await worker.stop()
162
+
163
+
164
+ if __name__ == "__main__":
165
+ asyncio.run(main())
backend/workers/queue_processing_worker.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Queue Processing Worker
3
+
4
+ Background worker that processes supervised execution queue.
5
+ Runs every 60 seconds to check for available users and process their queues.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from datetime import datetime
11
+
12
+ from core.database import get_db
13
+ from core.supervised_queue_service import SupervisedQueueService
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class QueueProcessingWorker:
19
+ """
20
+ Background worker for processing supervised execution queue.
21
+
22
+ Runs every 60 seconds to:
23
+ 1. Find users who recently became online/away
24
+ 2. Fetch their pending queues (batch of 10)
25
+ 3. Execute queued entries with supervision
26
+ 4. Handle expired queues (>24 hours)
27
+
28
+ Performance target: <5s per batch of 10 entries
29
+ """
30
+
31
+ def __init__(self, interval_seconds: int = 60):
32
+ self.interval_seconds = interval_seconds
33
+ self.running = False
34
+
35
+ async def run(self):
36
+ """Main worker loop."""
37
+ self.running = True
38
+ logger.info("QueueProcessingWorker started")
39
+
40
+ while self.running:
41
+ try:
42
+ start_time = datetime.now()
43
+
44
+ # Process pending queues
45
+ await self.process_pending_queues()
46
+
47
+ # Mark expired queues
48
+ await self.mark_expired_queues()
49
+
50
+ elapsed = (datetime.now() - start_time).total_seconds()
51
+ logger.info(
52
+ f"QueueProcessingWorker cycle completed in {elapsed:.2f}s"
53
+ )
54
+
55
+ except Exception as e:
56
+ logger.error(f"QueueProcessingWorker error: {e}", exc_info=True)
57
+
58
+ # Wait for next cycle
59
+ await asyncio.sleep(self.interval_seconds)
60
+
61
+ async def stop(self):
62
+ """Stop the worker."""
63
+ self.running = False
64
+ logger.info("QueueProcessingWorker stopped")
65
+
66
+ async def process_pending_queues(self):
67
+ """Process pending queue entries for available users."""
68
+ db = next(get_db())
69
+
70
+ try:
71
+ service = SupervisedQueueService(db)
72
+ processed = await service.process_pending_queues(limit=10)
73
+
74
+ if processed:
75
+ logger.info(
76
+ f"Processed {len(processed)} supervised queue entries"
77
+ )
78
+
79
+ finally:
80
+ db.close()
81
+
82
+ async def mark_expired_queues(self):
83
+ """Mark expired queue entries as failed."""
84
+ db = next(get_db())
85
+
86
+ try:
87
+ service = SupervisedQueueService(db)
88
+ count = await service.mark_expired_queues()
89
+
90
+ if count > 0:
91
+ logger.info(f"Marked {count} expired queues as failed")
92
+
93
+ finally:
94
+ db.close()
95
+
96
+
97
+ # ============================================================================
98
+ # Worker Entry Point
99
+ # ============================================================================
100
+
101
+ async def main():
102
+ """Entry point for running the worker."""
103
+ worker = QueueProcessingWorker(interval_seconds=60)
104
+
105
+ try:
106
+ await worker.run()
107
+ except KeyboardInterrupt:
108
+ logger.info("Received keyboard interrupt")
109
+ await worker.stop()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ asyncio.run(main())
backend/workers/reindex_graph_worker.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GraphRAG Redis Worker (Upstash)
3
+ Background process to run community detection (Leiden Algorithm) on PostgreSQL Graph.
4
+ Consumes jobs from 'graph_reindex_jobs' Redis queue.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import sys
11
+ import time
12
+ from typing import Any, Dict, List, Optional
13
+ import uuid
14
+ from sqlalchemy import text
15
+
16
+ # Add backend to path
17
+ sys.path.append(os.getcwd())
18
+
19
+ from core.database import SessionLocal, get_db_session
20
+ from core.models import CommunityMembership, GraphCommunity, GraphEdge, GraphNode
21
+ from core.service_factory import ServiceFactory
22
+ import asyncio
23
+
24
+ try:
25
+ import networkx as nx
26
+ except ImportError:
27
+ class MockGraph:
28
+ def __init__(self):
29
+ self._nodes = {}
30
+ self._edges = {}
31
+ @property
32
+ def nodes(self): return self._nodes
33
+ def add_node(self, id, **attr): self._nodes[id] = attr
34
+ def add_edge(self, u, v, **attr): self._edges[(u, v)] = attr
35
+ def number_of_nodes(self): return len(self._nodes)
36
+
37
+ class nx:
38
+ Graph = MockGraph
39
+ @staticmethod
40
+ def connected_components(G):
41
+ return [list(G.nodes.keys())]
42
+
43
+ # Configure Logging
44
+ logging.basicConfig(
45
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
46
+ level=logging.INFO
47
+ )
48
+ logger = logging.getLogger(__name__)
49
+
50
+ class RedisWorker:
51
+ def __init__(self, redis_url: str = None):
52
+ self.max_ram_nodes = 50000
53
+ self.queue_name = "graph_reindex_jobs"
54
+ self.redis_client = None
55
+
56
+ # Init Redis
57
+ redis_url = redis_url or os.getenv("UPSTASH_REDIS_URL") or os.getenv("REDIS_URL")
58
+ if redis_url:
59
+ try:
60
+ import redis
61
+ self.redis_client = redis.from_url(redis_url)
62
+ logger.info(f"Connected to Redis: {redis_url.split('@')[-1]}") # Log host only
63
+ except ImportError:
64
+ logger.warning("redis-py not installed.")
65
+ except Exception as e:
66
+ logger.error(f"Failed to connect to Redis: {e}")
67
+ else:
68
+ logger.warning("No REDIS_URL provided. Worker will not listen to queue.")
69
+
70
+ def fetch_graph(self, workspace_id: str) -> nx.Graph:
71
+ """Load entire workspace graph into NetworkX"""
72
+ session = SessionLocal()
73
+ G = nx.Graph()
74
+ try:
75
+ logger.info(f"Fetching nodes for workspace {workspace_id}...")
76
+ nodes = session.query(GraphNode.id, GraphNode.name).filter_by(workspace_id=workspace_id).all()
77
+ for n in nodes:
78
+ G.add_node(n.id, name=n.name)
79
+
80
+ logger.info(f"Fetching edges for workspace {workspace_id}...")
81
+ edges = session.query(GraphEdge.source_node_id, GraphEdge.target_node_id, GraphEdge.weight).filter_by(workspace_id=workspace_id).all()
82
+ for e in edges:
83
+ G.add_edge(e.source_node_id, e.target_node_id, weight=e.weight)
84
+
85
+ return G
86
+ finally:
87
+ session.close()
88
+
89
+ def detect_communities(self, G: nx.Graph) -> List[List[str]]:
90
+ """Run Louvain/Leiden algorithm"""
91
+ if G.number_of_nodes() == 0:
92
+ return []
93
+
94
+ try:
95
+ from networkx.algorithms.community import louvain_communities
96
+ logger.info(f"Running Louvain on {G.number_of_nodes()} nodes...")
97
+ communities = louvain_communities(G, seed=42)
98
+ return [list(c) for c in communities]
99
+ except ImportError:
100
+ logger.warning("Louvain not available, falling back to connected components")
101
+ return [list(c) for c in nx.connected_components(G)]
102
+
103
+ async def summarize_community(self, workspace_id: str, G: nx.Graph, community_nodes: List[str]) -> Dict[str, Any]:
104
+ """Generate LLM summary and keywords for a community using unified LLMService"""
105
+ llm = ServiceFactory.get_llm_service()
106
+
107
+ # Prepare context
108
+ nodes_list = [f"- {G.nodes[n].get('name', 'Unknown')} ({G.nodes[n].get('type', 'entity')})" for n in community_nodes[:20]]
109
+ nodes_str = "\n".join(nodes_list)
110
+
111
+ prompt = f"""Summarize this knowledge graph community of related entities.
112
+ Entities:
113
+ {nodes_str}
114
+
115
+ Respond in valid JSON only with this structure:
116
+ {{
117
+ "summary": "Short 1-2 sentence description emphasizing the common theme",
118
+ "keywords": ["keyword1", "keyword2", "keyword3"]
119
+ }}"""
120
+
121
+ try:
122
+ # LLMService handles tenant isolation and usage tracking automatically
123
+ result = await llm.generate_response(
124
+ prompt=prompt,
125
+ tenant_id=workspace_id,
126
+ system_prompt="You are a GraphRAG Community Analyst. Categorize and summarize groups of entities.",
127
+ json_mode=True
128
+ )
129
+
130
+ data = json.loads(result)
131
+ return {
132
+ "summary": data.get("summary", f"Community of {len(community_nodes)} entities."),
133
+ "keywords": data.get("keywords", [])
134
+ }
135
+ except Exception as e:
136
+ logger.error(f"Failed to summarize community via LLMService: {e}")
137
+ node_names = [G.nodes[n].get("name", "Unknown") for n in community_nodes[:3]]
138
+ return {
139
+ "summary": f"Group related to {', '.join(node_names)}.",
140
+ "keywords": node_names
141
+ }
142
+
143
+ def save_communities(self, workspace_id: str, communities: List[List[str]], G: nx.Graph):
144
+ """Persist results to Postgres"""
145
+ session = SessionLocal()
146
+ try:
147
+ session.execute(text("DELETE FROM graph_communities WHERE workspace_id = :ws_id"), {"ws_id": workspace_id})
148
+ session.commit()
149
+
150
+ logger.info(f"Summarizing and saving {len(communities)} communities...")
151
+ for i, members in enumerate(communities):
152
+ if len(members) < 2: continue
153
+
154
+ # Perform async LLM summarization
155
+ res = asyncio.run(self.summarize_community(workspace_id, G, members))
156
+
157
+ comm = GraphCommunity(
158
+ workspace_id=workspace_id,
159
+ level=0,
160
+ summary=res["summary"],
161
+ keywords=res["keywords"]
162
+ )
163
+ session.add(comm)
164
+ session.flush() # Get ID
165
+
166
+ for node_id in members:
167
+ membership = CommunityMembership(
168
+ community_id=comm.id,
169
+ node_id=node_id
170
+ )
171
+ session.add(membership)
172
+
173
+ session.commit()
174
+ logger.info(f"Saved {len(communities)} communities for workspace {workspace_id}")
175
+
176
+ except Exception as e:
177
+ session.rollback()
178
+ logger.error(f"Failed to save communities: {e}")
179
+ finally:
180
+ session.close()
181
+
182
+ def process_job(self, workspace_id: str):
183
+ logger.info(f"WORKER: Starting job for {workspace_id}")
184
+ G = self.fetch_graph(workspace_id)
185
+ if G.number_of_nodes() > self.max_ram_nodes:
186
+ logger.error(f"Graph too large ({G.number_of_nodes()} nodes).")
187
+ return
188
+
189
+ communities = self.detect_communities(G)
190
+ self.save_communities(workspace_id, communities, G)
191
+ logger.info("WORKER: Job Finished.")
192
+
193
+ def run(self):
194
+ """Main listening loop"""
195
+ if not self.redis_client:
196
+ logger.error("Redis not connected. Exiting.")
197
+ return
198
+
199
+ logger.info(f"Listening on queue: {self.queue_name}...")
200
+
201
+ while True:
202
+ # Scale-to-Zero logic: If fetch returns None after timeout, exit
203
+ # For now, block indefinitely or use timeout
204
+ try:
205
+ # brpop returns tuple (queue_name, value)
206
+ job = self.redis_client.brpop(self.queue_name, timeout=30)
207
+
208
+ if job:
209
+ _, workspace_id_bytes = job
210
+ workspace_id = workspace_id_bytes.decode('utf-8')
211
+ self.process_job(workspace_id)
212
+ else:
213
+ logger.info("Queue empty (timeout). Idle...")
214
+ # In production with 'machines on demand', we would exit here
215
+ # sys.exit(0)
216
+ except Exception as e:
217
+ logger.error(f"Worker Error: {e}")
218
+ time.sleep(5)
219
+
220
+ if __name__ == "__main__":
221
+ # If run with argument, process single job (Manual/Test mode)
222
+ if len(sys.argv) > 1:
223
+ workspace_id = sys.argv[1]
224
+ worker = RedisWorker(redis_url="mock://") # Skip redis conn
225
+ worker.process_job(workspace_id)
226
+ else:
227
+ # Run in Daemon mode
228
+ worker = RedisWorker()
229
+ worker.run()
backend/workers/social_media_worker.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Social Media Worker for Background Post Processing
3
+
4
+ Handles scheduled social media posts in the background using RQ.
5
+ Processes posts at their scheduled time and logs results to the database.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime
10
+ from typing import Dict, List, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ async def process_scheduled_post(
16
+ post_id: str,
17
+ platforms: List[str],
18
+ text: str,
19
+ scheduled_for: datetime,
20
+ media_urls: Optional[List[str]] = None,
21
+ link_url: Optional[str] = None,
22
+ user_id: Optional[str] = None
23
+ ) -> Dict[str, any]:
24
+ """
25
+ Process a scheduled social media post.
26
+
27
+ This function is called by the RQ worker when a scheduled post is due.
28
+ It posts to each platform and logs the results.
29
+
30
+ Args:
31
+ post_id: Unique identifier for the post
32
+ platforms: List of platform names (twitter, linkedin, facebook)
33
+ text: Post content
34
+ scheduled_for: When the post was scheduled (for logging)
35
+ media_urls: Optional list of image/video URLs
36
+ link_url: Optional link to include in the post
37
+ user_id: User ID for tracking
38
+
39
+ Returns:
40
+ Dictionary with platform results
41
+ """
42
+ from core.database import SessionLocal
43
+ from core.models import SocialPostHistory, OAuthToken
44
+
45
+ db = SessionLocal()
46
+
47
+ try:
48
+ logger.info(f"Processing scheduled post {post_id} for {len(platforms)} platforms")
49
+
50
+ # Update status to processing
51
+ history = db.query(SocialPostHistory).filter(
52
+ SocialPostHistory.post_id == post_id
53
+ ).first()
54
+
55
+ if not history:
56
+ logger.error(f"SocialPostHistory record not found for post_id={post_id}")
57
+ return {"error": "Post not found in database"}
58
+
59
+ history.status = "posting"
60
+ history.posted_at = datetime.utcnow()
61
+ db.commit()
62
+
63
+ # Get platform poster functions
64
+ from api.social_media_routes import post_to_twitter, post_to_linkedin, post_to_facebook
65
+
66
+ platform_posters = {
67
+ "twitter": post_to_twitter,
68
+ "linkedin": post_to_linkedin,
69
+ "facebook": post_to_facebook,
70
+ }
71
+
72
+ platform_results = {}
73
+ successful_posts = 0
74
+
75
+ # Post to each platform
76
+ for platform in platforms:
77
+ platform = platform.lower()
78
+
79
+ logger.info(f"Posting to {platform} for post {post_id}")
80
+
81
+ try:
82
+ # Get OAuth token for this platform and user
83
+ oauth_token = db.query(OAuthToken).filter(
84
+ OAuthToken.user_id == user_id,
85
+ OAuthToken.provider == platform,
86
+ OAuthToken.status == "active"
87
+ ).first()
88
+
89
+ if not oauth_token:
90
+ platform_results[platform] = {
91
+ "success": False,
92
+ "error": f"No active {platform} account connected. Please connect your account first."
93
+ }
94
+ logger.warning(f"No OAuth token found for {platform}")
95
+ continue
96
+
97
+ # Get the poster function
98
+ poster_func = platform_posters.get(platform)
99
+
100
+ if not poster_func:
101
+ platform_results[platform] = {
102
+ "success": False,
103
+ "error": f"Platform {platform} not yet implemented"
104
+ }
105
+ logger.warning(f"No poster function for {platform}")
106
+ continue
107
+
108
+ # Post to platform
109
+ result = await poster_func(
110
+ text=text,
111
+ access_token=oauth_token.access_token,
112
+ media_urls=media_urls,
113
+ link_url=link_url
114
+ )
115
+
116
+ platform_results[platform] = result
117
+
118
+ if result.get("success"):
119
+ successful_posts += 1
120
+ logger.info(f"Successfully posted to {platform}")
121
+ else:
122
+ logger.error(f"Failed to post to {platform}: {result.get('error')}")
123
+
124
+ # Update last_used timestamp
125
+ oauth_token.last_used = datetime.utcnow()
126
+
127
+ except Exception as e:
128
+ logger.error(f"Error posting to {platform}: {e}", exc_info=True)
129
+ platform_results[platform] = {
130
+ "success": False,
131
+ "error": str(e)
132
+ }
133
+
134
+ # Update history with results
135
+ if successful_posts == len(platforms):
136
+ history.status = "posted"
137
+ logger.info(f"Post {post_id} successfully posted to all platforms")
138
+ elif successful_posts > 0:
139
+ history.status = "partial"
140
+ logger.warning(f"Post {post_id} partially posted ({successful_posts}/{len(platforms)})")
141
+ else:
142
+ history.status = "failed"
143
+ history.error_message = "Failed to post to any platform"
144
+ logger.error(f"Post {post_id} failed completely")
145
+
146
+ history.platform_results = platform_results
147
+ history.posted_at = datetime.utcnow()
148
+ db.commit()
149
+
150
+ return {
151
+ "success": successful_posts > 0,
152
+ "post_id": post_id,
153
+ "platforms": platforms,
154
+ "platform_results": platform_results,
155
+ "successful_posts": successful_posts,
156
+ "total_platforms": len(platforms),
157
+ "status": history.status
158
+ }
159
+
160
+ except Exception as e:
161
+ logger.error(f"Failed to process scheduled post {post_id}: {e}", exc_info=True)
162
+
163
+ # Update history to failed
164
+ try:
165
+ history = db.query(SocialPostHistory).filter(
166
+ SocialPostHistory.post_id == post_id
167
+ ).first()
168
+
169
+ if history:
170
+ history.status = "failed"
171
+ history.error_message = str(e)
172
+ db.commit()
173
+ except Exception as db_error:
174
+ logger.error(f"Failed to update history: {db_error}")
175
+
176
+ raise
177
+
178
+ finally:
179
+ db.close()
180
+
181
+
182
+ def process_scheduled_post_sync(*args, **kwargs):
183
+ """
184
+ Synchronous wrapper for process_scheduled_post.
185
+
186
+ RQ workers require synchronous functions, so this wrapper
187
+ handles the async execution properly.
188
+ """
189
+ import asyncio
190
+
191
+ try:
192
+ # Get or create event loop
193
+ loop = asyncio.get_event_loop()
194
+ except RuntimeError:
195
+ # No event loop in this thread
196
+ loop = asyncio.new_event_loop()
197
+ asyncio.set_event_loop(loop)
198
+
199
+ return loop.run_until_complete(process_scheduled_post(*args, **kwargs))
backend/zero_coverage_categorized.json ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "categories": {
3
+ "CRITICAL": [
4
+ {
5
+ "file": "core/workflow_versioning_system.py",
6
+ "lines": 442,
7
+ "missing": 442
8
+ },
9
+ {
10
+ "file": "core/workflow_marketplace.py",
11
+ "lines": 332,
12
+ "missing": 332
13
+ },
14
+ {
15
+ "file": "core/advanced_workflow_endpoints.py",
16
+ "lines": 265,
17
+ "missing": 265
18
+ },
19
+ {
20
+ "file": "core/workflow_template_endpoints.py",
21
+ "lines": 243,
22
+ "missing": 243
23
+ },
24
+ {
25
+ "file": "api/workflow_versioning_endpoints.py",
26
+ "lines": 228,
27
+ "missing": 228
28
+ },
29
+ {
30
+ "file": "core/graduation_exam.py",
31
+ "lines": 227,
32
+ "missing": 227
33
+ },
34
+ {
35
+ "file": "core/enterprise_user_management.py",
36
+ "lines": 208,
37
+ "missing": 208
38
+ },
39
+ {
40
+ "file": "core/reconciliation_engine.py",
41
+ "lines": 164,
42
+ "missing": 164
43
+ },
44
+ {
45
+ "file": "core/constitutional_validator.py",
46
+ "lines": 157,
47
+ "missing": 157
48
+ }
49
+ ],
50
+ "HIGH": [
51
+ {
52
+ "file": "api/smarthome_routes.py",
53
+ "lines": 188,
54
+ "missing": 188
55
+ },
56
+ {
57
+ "file": "api/creative_routes.py",
58
+ "lines": 157,
59
+ "missing": 157
60
+ },
61
+ {
62
+ "file": "api/productivity_routes.py",
63
+ "lines": 156,
64
+ "missing": 156
65
+ }
66
+ ],
67
+ "MEDIUM": [
68
+ {
69
+ "file": "core/apar_engine.py",
70
+ "lines": 177,
71
+ "missing": 177
72
+ },
73
+ {
74
+ "file": "core/byok_cost_optimizer.py",
75
+ "lines": 168,
76
+ "missing": 168
77
+ },
78
+ {
79
+ "file": "core/local_ocr_service.py",
80
+ "lines": 164,
81
+ "missing": 164
82
+ },
83
+ {
84
+ "file": "core/debug_alerting.py",
85
+ "lines": 155,
86
+ "missing": 155
87
+ },
88
+ {
89
+ "file": "core/budget_enforcement_service.py",
90
+ "lines": 151,
91
+ "missing": 151
92
+ },
93
+ {
94
+ "file": "core/logging_config.py",
95
+ "lines": 148,
96
+ "missing": 148
97
+ },
98
+ {
99
+ "file": "core/formula_memory.py",
100
+ "lines": 147,
101
+ "missing": 147
102
+ },
103
+ {
104
+ "file": "core/communication_service.py",
105
+ "lines": 145,
106
+ "missing": 145
107
+ },
108
+ {
109
+ "file": "core/scheduler.py",
110
+ "lines": 144,
111
+ "missing": 144
112
+ }
113
+ ],
114
+ "LOW": [
115
+ {
116
+ "file": "api/debug_routes.py",
117
+ "lines": 296,
118
+ "missing": 296
119
+ },
120
+ {
121
+ "file": "core/industry_workflow_endpoints.py",
122
+ "lines": 181,
123
+ "missing": 181
124
+ },
125
+ {
126
+ "file": "core/oauth_user_context.py",
127
+ "lines": 142,
128
+ "missing": 142
129
+ },
130
+ {
131
+ "file": "core/ai_workflow_optimization_endpoints.py",
132
+ "lines": 137,
133
+ "missing": 137
134
+ },
135
+ {
136
+ "file": "core/byok_competitive_endpoints.py",
137
+ "lines": 137,
138
+ "missing": 137
139
+ },
140
+ {
141
+ "file": "core/error_middleware.py",
142
+ "lines": 137,
143
+ "missing": 137
144
+ },
145
+ {
146
+ "file": "core/local_llm_secrets_detector.py",
147
+ "lines": 137,
148
+ "missing": 137
149
+ },
150
+ {
151
+ "file": "core/agent_execution_service.py",
152
+ "lines": 134,
153
+ "missing": 134
154
+ },
155
+ {
156
+ "file": "core/analytics_engine.py",
157
+ "lines": 130,
158
+ "missing": 130
159
+ },
160
+ {
161
+ "file": "core/governance_helper.py",
162
+ "lines": 130,
163
+ "missing": 130
164
+ },
165
+ {
166
+ "file": "core/competitive_advantage_dashboard.py",
167
+ "lines": 123,
168
+ "missing": 123
169
+ },
170
+ {
171
+ "file": "core/debug_streaming.py",
172
+ "lines": 123,
173
+ "missing": 123
174
+ },
175
+ {
176
+ "file": "tools/calendar_tool.py",
177
+ "lines": 123,
178
+ "missing": 123
179
+ },
180
+ {
181
+ "file": "core/mcp_service.py",
182
+ "lines": 122,
183
+ "missing": 122
184
+ },
185
+ {
186
+ "file": "core/background_agent_runner.py",
187
+ "lines": 121,
188
+ "missing": 121
189
+ },
190
+ {
191
+ "file": "core/chronological_integrity.py",
192
+ "lines": 120,
193
+ "missing": 120
194
+ },
195
+ {
196
+ "file": "core/analytics_endpoints.py",
197
+ "lines": 119,
198
+ "missing": 119
199
+ },
200
+ {
201
+ "file": "core/package_governance_service.py",
202
+ "lines": 119,
203
+ "missing": 119
204
+ },
205
+ {
206
+ "file": "core/health_monitor.py",
207
+ "lines": 113,
208
+ "missing": 113
209
+ },
210
+ {
211
+ "file": "core/active_intervention_service.py",
212
+ "lines": 112,
213
+ "missing": 112
214
+ },
215
+ {
216
+ "file": "core/financial_audit_orchestrator.py",
217
+ "lines": 112,
218
+ "missing": 112
219
+ },
220
+ {
221
+ "file": "core/governance_wrapper.py",
222
+ "lines": 111,
223
+ "missing": 111
224
+ },
225
+ {
226
+ "file": "core/token_refresher.py",
227
+ "lines": 107,
228
+ "missing": 107
229
+ },
230
+ {
231
+ "file": "core/uptime_tracker.py",
232
+ "lines": 103,
233
+ "missing": 103
234
+ },
235
+ {
236
+ "file": "core/database_helper.py",
237
+ "lines": 102,
238
+ "missing": 102
239
+ },
240
+ {
241
+ "file": "core/user_context_manager.py",
242
+ "lines": 102,
243
+ "missing": 102
244
+ }
245
+ ]
246
+ },
247
+ "modules": {
248
+ "core": 41,
249
+ "api": 5,
250
+ "tools": 1,
251
+ "cli": 0,
252
+ "other": 0
253
+ },
254
+ "coverage_potential": {
255
+ "wave3_critical": {
256
+ "files": 9,
257
+ "lines": 2266,
258
+ "pct": 3.061417493042233
259
+ },
260
+ "wave4_high": {
261
+ "files": 3,
262
+ "lines": 501,
263
+ "pct": 0.6768623848253128
264
+ },
265
+ "wave5_medium": {
266
+ "files": 9,
267
+ "lines": 1399,
268
+ "pct": 1.8900807911589075
269
+ },
270
+ "wave6_low": {
271
+ "files": 26,
272
+ "lines": 3393,
273
+ "pct": 4.584020103218136
274
+ }
275
+ }
276
+ }
backend/zero_coverage_files_analysis.json ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "file": "core/workflow_versioning_system.py",
4
+ "lines": 442,
5
+ "missing": 442
6
+ },
7
+ {
8
+ "file": "core/workflow_marketplace.py",
9
+ "lines": 332,
10
+ "missing": 332
11
+ },
12
+ {
13
+ "file": "api/debug_routes.py",
14
+ "lines": 296,
15
+ "missing": 296
16
+ },
17
+ {
18
+ "file": "core/advanced_workflow_endpoints.py",
19
+ "lines": 265,
20
+ "missing": 265
21
+ },
22
+ {
23
+ "file": "core/workflow_template_endpoints.py",
24
+ "lines": 243,
25
+ "missing": 243
26
+ },
27
+ {
28
+ "file": "api/workflow_versioning_endpoints.py",
29
+ "lines": 228,
30
+ "missing": 228
31
+ },
32
+ {
33
+ "file": "core/graduation_exam.py",
34
+ "lines": 227,
35
+ "missing": 227
36
+ },
37
+ {
38
+ "file": "core/enterprise_user_management.py",
39
+ "lines": 208,
40
+ "missing": 208
41
+ },
42
+ {
43
+ "file": "api/smarthome_routes.py",
44
+ "lines": 188,
45
+ "missing": 188
46
+ },
47
+ {
48
+ "file": "core/industry_workflow_endpoints.py",
49
+ "lines": 181,
50
+ "missing": 181
51
+ },
52
+ {
53
+ "file": "core/apar_engine.py",
54
+ "lines": 177,
55
+ "missing": 177
56
+ },
57
+ {
58
+ "file": "core/byok_cost_optimizer.py",
59
+ "lines": 168,
60
+ "missing": 168
61
+ },
62
+ {
63
+ "file": "core/local_ocr_service.py",
64
+ "lines": 164,
65
+ "missing": 164
66
+ },
67
+ {
68
+ "file": "core/reconciliation_engine.py",
69
+ "lines": 164,
70
+ "missing": 164
71
+ },
72
+ {
73
+ "file": "api/creative_routes.py",
74
+ "lines": 157,
75
+ "missing": 157
76
+ },
77
+ {
78
+ "file": "core/constitutional_validator.py",
79
+ "lines": 157,
80
+ "missing": 157
81
+ },
82
+ {
83
+ "file": "api/productivity_routes.py",
84
+ "lines": 156,
85
+ "missing": 156
86
+ },
87
+ {
88
+ "file": "core/debug_alerting.py",
89
+ "lines": 155,
90
+ "missing": 155
91
+ },
92
+ {
93
+ "file": "core/budget_enforcement_service.py",
94
+ "lines": 151,
95
+ "missing": 151
96
+ },
97
+ {
98
+ "file": "core/logging_config.py",
99
+ "lines": 148,
100
+ "missing": 148
101
+ },
102
+ {
103
+ "file": "core/formula_memory.py",
104
+ "lines": 147,
105
+ "missing": 147
106
+ },
107
+ {
108
+ "file": "core/communication_service.py",
109
+ "lines": 145,
110
+ "missing": 145
111
+ },
112
+ {
113
+ "file": "core/scheduler.py",
114
+ "lines": 144,
115
+ "missing": 144
116
+ },
117
+ {
118
+ "file": "core/oauth_user_context.py",
119
+ "lines": 142,
120
+ "missing": 142
121
+ },
122
+ {
123
+ "file": "core/ai_workflow_optimization_endpoints.py",
124
+ "lines": 137,
125
+ "missing": 137
126
+ },
127
+ {
128
+ "file": "core/byok_competitive_endpoints.py",
129
+ "lines": 137,
130
+ "missing": 137
131
+ },
132
+ {
133
+ "file": "core/error_middleware.py",
134
+ "lines": 137,
135
+ "missing": 137
136
+ },
137
+ {
138
+ "file": "core/local_llm_secrets_detector.py",
139
+ "lines": 137,
140
+ "missing": 137
141
+ },
142
+ {
143
+ "file": "core/agent_execution_service.py",
144
+ "lines": 134,
145
+ "missing": 134
146
+ },
147
+ {
148
+ "file": "core/analytics_engine.py",
149
+ "lines": 130,
150
+ "missing": 130
151
+ },
152
+ {
153
+ "file": "core/governance_helper.py",
154
+ "lines": 130,
155
+ "missing": 130
156
+ },
157
+ {
158
+ "file": "core/competitive_advantage_dashboard.py",
159
+ "lines": 123,
160
+ "missing": 123
161
+ },
162
+ {
163
+ "file": "core/debug_streaming.py",
164
+ "lines": 123,
165
+ "missing": 123
166
+ },
167
+ {
168
+ "file": "tools/calendar_tool.py",
169
+ "lines": 123,
170
+ "missing": 123
171
+ },
172
+ {
173
+ "file": "core/mcp_service.py",
174
+ "lines": 122,
175
+ "missing": 122
176
+ },
177
+ {
178
+ "file": "core/background_agent_runner.py",
179
+ "lines": 121,
180
+ "missing": 121
181
+ },
182
+ {
183
+ "file": "core/chronological_integrity.py",
184
+ "lines": 120,
185
+ "missing": 120
186
+ },
187
+ {
188
+ "file": "core/analytics_endpoints.py",
189
+ "lines": 119,
190
+ "missing": 119
191
+ },
192
+ {
193
+ "file": "core/package_governance_service.py",
194
+ "lines": 119,
195
+ "missing": 119
196
+ },
197
+ {
198
+ "file": "core/health_monitor.py",
199
+ "lines": 113,
200
+ "missing": 113
201
+ },
202
+ {
203
+ "file": "core/active_intervention_service.py",
204
+ "lines": 112,
205
+ "missing": 112
206
+ },
207
+ {
208
+ "file": "core/financial_audit_orchestrator.py",
209
+ "lines": 112,
210
+ "missing": 112
211
+ },
212
+ {
213
+ "file": "core/governance_wrapper.py",
214
+ "lines": 111,
215
+ "missing": 111
216
+ },
217
+ {
218
+ "file": "core/token_refresher.py",
219
+ "lines": 107,
220
+ "missing": 107
221
+ },
222
+ {
223
+ "file": "core/uptime_tracker.py",
224
+ "lines": 103,
225
+ "missing": 103
226
+ },
227
+ {
228
+ "file": "core/database_helper.py",
229
+ "lines": 102,
230
+ "missing": 102
231
+ },
232
+ {
233
+ "file": "core/user_context_manager.py",
234
+ "lines": 102,
235
+ "missing": 102
236
+ }
237
+ ]