Spaces:
Running
Running
Full ATOM backend sync from D:/Annator/ATOM/atom/backend + HF slim runtime (part 3)
Browse files- DEPLOY_STAMP.txt +1 -0
- backend/token_status.txt +0 -0
- backend/workers/__init__.py +0 -0
- backend/workers/activity_state_worker.py +165 -0
- backend/workers/queue_processing_worker.py +113 -0
- backend/workers/reindex_graph_worker.py +229 -0
- backend/workers/social_media_worker.py +199 -0
- backend/zero_coverage_categorized.json +276 -0
- backend/zero_coverage_files_analysis.json +237 -0
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 |
+
]
|