Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-12 09:36:17

0001 """Capcom endpoints: display-ready SWF state and buffered SWF events.
0002 
0003 Capcom polls the open read endpoints every few minutes (through the
0004 swf-remote proxy for external reach). Each entry under 'states' is shaped
0005 exactly as tjai's capcom.set_state(source, value, color, url) expects,
0006 following the pax-eden Ahbazon producer, so the capcom-side collector can
0007 apply each entry as delivered.
0008 
0009 Discrete SWF events (the campaign-delivery and task-operation feeds) are
0010 buffered here in CapcomNotice rows and served by the open notices
0011 endpoint; the consumer drains them with a since-cursor on its own poll.
0012 SWF never posts into an external feed and holds no external credential —
0013 the producing agent writes notices to this monitor with its ordinary
0014 monitor token, and the feed system's credentials stay entirely on the
0015 feed system's side.
0016 """
0017 
0018 import logging
0019 import re
0020 from datetime import datetime, timedelta
0021 from datetime import timezone as datetime_timezone
0022 from urllib.parse import urlencode
0023 
0024 from django.http import JsonResponse
0025 from django.utils import timezone
0026 from rest_framework.authentication import TokenAuthentication
0027 from rest_framework.decorators import (api_view, authentication_classes,
0028                                        permission_classes)
0029 from rest_framework.permissions import IsAuthenticated
0030 from rest_framework.response import Response
0031 
0032 logger = logging.getLogger(__name__)
0033 
0034 # The notice buffer is a hand-off, not an archive: consumers keep their
0035 # own history, so rows past this window are pruned on each ingest.
0036 NOTICE_RETENTION_DAYS = 30
0037 # Page cap on one notices read; 'more' flags a truncated response.
0038 NOTICE_PAGE_MAX = 500
0039 
0040 REMOTE_FACE = 'https://epic-devcloud.org/prod'
0041 CAPCOM_WORKLOAD_CHECKS = frozenset({'stale-state'})
0042 USERNAME_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$')
0043 CAPCOM_VALUE_MAX_CHARS = 50
0044 
0045 
0046 def _compact_value(value):
0047     """Keep state-tile summaries within Capcom's usual 50 characters."""
0048     value = str(value)
0049     if len(value) <= CAPCOM_VALUE_MAX_CHARS:
0050         return value
0051     return value[:CAPCOM_VALUE_MAX_CHARS - 1].rstrip() + '…'
0052 
0053 
0054 def _running_panda_task_count():
0055     """Return the count of PanDA tasks currently in status running."""
0056     from django.db import connections
0057     from ..panda.queries import PANDA_SCHEMA
0058 
0059     connection = connections['panda']
0060     with connection.cursor() as cursor:
0061         cursor.execute(
0062             f'SELECT COUNT(*) FROM "{PANDA_SCHEMA}"."jedi_tasks" '
0063             'WHERE "status" = %s',
0064             ['running'],
0065         )
0066         return int(cursor.fetchone()[0] or 0)
0067 
0068 
0069 def _paused_panda_tasks(limit=20):
0070     """Return the complete paused count plus recent task detail."""
0071     from django.db import connections
0072     from ..panda.queries import PANDA_SCHEMA
0073 
0074     connection = connections['panda']
0075     with connection.cursor() as cursor:
0076         cursor.execute(
0077             f'SELECT COUNT(*) FROM "{PANDA_SCHEMA}"."jedi_tasks" '
0078             'WHERE "status" = %s',
0079             ['paused'],
0080         )
0081         count = int(cursor.fetchone()[0] or 0)
0082         cursor.execute(
0083             f'SELECT "jeditaskid", "taskname", "username", '
0084             f'"modificationtime" FROM "{PANDA_SCHEMA}"."jedi_tasks" '
0085             'WHERE "status" = %s ORDER BY "modificationtime" DESC '
0086             'LIMIT %s',
0087             ['paused', limit],
0088         )
0089         tasks = [
0090             {
0091                 'jedi_task_id': row[0],
0092                 'task_name': row[1] or '',
0093                 'username': row[2] or '',
0094                 'modified_at': row[3].isoformat() if row[3] else None,
0095             }
0096             for row in cursor.fetchall()
0097         ]
0098     return count, tasks
0099 
0100 
0101 def _user_testbed_summary(username, now):
0102     """Return display and detail state for one user's SWF testbed."""
0103     from ..models import SysConfig, SystemAgent
0104     from ..workflow_models import Namespace, WorkflowExecution
0105 
0106     healthy_after = now - timedelta(minutes=5)
0107     stale_hours = float(SysConfig.get_setting('state_stale_hours', 12))
0108     stale_before = now - timedelta(hours=stale_hours)
0109 
0110     manager = SystemAgent.objects.filter(
0111         instance_name=f'agent-manager-{username}').first()
0112     executions = WorkflowExecution.objects.filter(executed_by=username)
0113     latest_execution = executions.order_by('-start_time').first()
0114 
0115     manager_is_fresh = bool(
0116         manager and manager.last_heartbeat
0117         and manager.last_heartbeat >= healthy_after
0118         and manager.operational_state != 'EXITED')
0119     owned_namespace = (Namespace.objects.filter(owner=username)
0120                        .order_by('-updated_at')
0121                        .values_list('name', flat=True).first())
0122     namespace = None
0123     if manager_is_fresh and manager.namespace:
0124         namespace = manager.namespace
0125     elif latest_execution and latest_execution.namespace:
0126         namespace = latest_execution.namespace
0127     elif manager and manager.namespace:
0128         namespace = manager.namespace
0129     else:
0130         namespace = owned_namespace
0131 
0132     agents = []
0133     if namespace:
0134         agents = list(SystemAgent.objects.filter(namespace=namespace)
0135                       .exclude(instance_name=f'agent-manager-{username}')
0136                       .exclude(operational_state='EXITED')
0137                       .order_by('-last_heartbeat'))
0138     fresh_agents = [
0139         agent for agent in agents
0140         if agent.last_heartbeat and agent.last_heartbeat >= healthy_after
0141     ]
0142     stale_agents = [agent for agent in agents if agent not in fresh_agents]
0143     error_agents = [agent for agent in fresh_agents if agent.status == 'ERROR']
0144     running_workflows = executions.filter(status='running').count()
0145     stale_workflows = executions.filter(
0146         status='running', end_time__isnull=True,
0147         start_time__lt=stale_before).count()
0148 
0149     if stale_workflows:
0150         color = 'yellow'
0151     elif error_agents or (manager_is_fresh and manager.status == 'ERROR'):
0152         color = 'red'
0153     elif running_workflows:
0154         color = 'green'
0155     elif fresh_agents:
0156         color = 'green'
0157     else:
0158         color = None
0159     display_count = len(fresh_agents)
0160     label = f'testbed {display_count}'
0161 
0162     return {
0163         'label': label,
0164         'color': color,
0165         'namespace': namespace,
0166         'agent_manager': {
0167             'alive': manager_is_fresh,
0168             'status': manager.status if manager else None,
0169             'last_heartbeat': (
0170                 manager.last_heartbeat.isoformat()
0171                 if manager and manager.last_heartbeat else None),
0172         },
0173         'agents': {
0174             'display_count': display_count,
0175             'fresh': len(fresh_agents),
0176             'stale': len(stale_agents),
0177             'error': len(error_agents),
0178         },
0179         'workflows': {
0180             'running': running_workflows,
0181             'stale': stale_workflows,
0182             'stale_after_hours': stale_hours,
0183         },
0184         'last_execution': ({
0185             'execution_id': latest_execution.execution_id,
0186             'status': latest_execution.status,
0187             'start_time': (latest_execution.start_time.isoformat()
0188                            if latest_execution.start_time else None),
0189             'end_time': (latest_execution.end_time.isoformat()
0190                          if latest_execution.end_time else None),
0191         } if latest_execution else None),
0192     }
0193 
0194 
0195 def _user_panda_summary(username):
0196     """Return a compact trailing-day PanDA summary for one effective user."""
0197     from ..panda.queries import get_activity
0198 
0199     activity = get_activity(days=1, username=username)
0200     if activity.get('error'):
0201         raise RuntimeError(activity['error'])
0202     jobs = activity.get('jobs') or {}
0203     tasks = activity.get('tasks') or {}
0204     job_status = jobs.get('by_status') or {}
0205     task_status = tasks.get('by_status') or {}
0206 
0207     running_jobs = int(job_status.get('running', 0) or 0)
0208     terminal_tasks = {'done', 'failed', 'aborted', 'broken', 'finished'}
0209     active_tasks = sum(
0210         int(count or 0) for status, count in task_status.items()
0211         if status not in terminal_tasks)
0212     finished_jobs = int(job_status.get('finished', 0) or 0)
0213     failed_jobs = sum(int(job_status.get(status, 0) or 0)
0214                       for status in ('failed', 'cancelled', 'closed'))
0215 
0216     if running_jobs:
0217         display_count = running_jobs
0218     elif active_tasks:
0219         display_count = active_tasks
0220     else:
0221         display_count = finished_jobs + failed_jobs
0222     label = f'PanDA {display_count}'
0223 
0224     return {
0225         'label': label,
0226         'window_hours': 24,
0227         'display_count': display_count,
0228         'running_jobs': running_jobs,
0229         'active_tasks': active_tasks,
0230         'finished_jobs': finished_jobs,
0231         'failed_jobs': failed_jobs,
0232         'jobs_by_status': job_status,
0233         'tasks_by_status': task_status,
0234     }
0235 
0236 
0237 def capcom_state(request):
0238     """SWF state tiles: the System page verdict and current PanDA activity.
0239 
0240     states: tile-exact entries (source, value, color, url).
0241     detail: the numbers behind them, for any richer capcom-side use.
0242     Each section degrades independently: a failing source contributes an
0243     error tile rather than hiding or failing the whole response.
0244     """
0245     from ..snapper_panda import _in_flight_activity, _terminal_outcome_rows
0246     from ..system_status import status_summary
0247 
0248     now = timezone.now()
0249     states = []
0250     detail = {}
0251 
0252     try:
0253         summary = status_summary(exclude_names=CAPCOM_WORKLOAD_CHECKS)
0254         status = summary.get('overall_status', 'unknown')
0255         bad = int(summary.get('warning', 0)) + int(summary.get('error', 0))
0256         value = status.upper() + (f' ({bad})' if bad else '')
0257         color = {'ok': 'green', 'warning': 'yellow',
0258                  'error': 'red'}.get(status)
0259         entry = {'source': 'swf-system', 'value': value,
0260                  'url': f'{REMOTE_FACE}/system/'}
0261         if color:
0262             entry['color'] = color
0263         states.append(entry)
0264         latest = summary.get('latest_checked_at')
0265         detail['system'] = {
0266             'scope': 'infrastructure-operations',
0267             'excluded_checks': sorted(CAPCOM_WORKLOAD_CHECKS),
0268             'status': status,
0269             'reason': summary.get('overall_reason', ''),
0270             'ok': summary.get('ok', 0),
0271             'warning': summary.get('warning', 0),
0272             'error': summary.get('error', 0),
0273             'checked_at': latest.isoformat() if latest else None,
0274         }
0275     except Exception as exc:
0276         logger.error('capcom state: system summary failed: %s', exc)
0277         states.append({'source': 'swf-system', 'value': 'UNAVAILABLE',
0278                        'url': f'{REMOTE_FACE}/system/'})
0279         detail['system'] = {'error_text': str(exc)}
0280 
0281     try:
0282         running_jobs = sum(
0283             row['jobs'] for row in _in_flight_activity()
0284             if row['status'] == 'running')
0285         finished = 0
0286         failed = 0
0287         for _site, status, _cls, count in _terminal_outcome_rows(
0288                 now - timedelta(hours=12), now):
0289             if status == 'finished':
0290                 finished += int(count or 0)
0291             elif status == 'failed':
0292                 failed += int(count or 0)
0293         decided = finished + failed
0294         pct = round(100.0 * finished / decided, 1) if decided else None
0295         running_tasks = _running_panda_task_count()
0296         paused_count, paused_tasks = _paused_panda_tasks()
0297         value_parts = [f'{running_jobs} jobs', f'{running_tasks} tasks',
0298                        f'{paused_count} paused']
0299         if pct is not None:
0300             value_parts.append(f'{pct:.0f}%')
0301         entry = {
0302             'source': 'swf-panda',
0303             'value': _compact_value(' · '.join(value_parts)),
0304             'url': f'{REMOTE_FACE}/panda/jobs/',
0305         }
0306         if paused_count:
0307             entry['color'] = 'yellow'
0308         states.append(entry)
0309         detail['panda'] = {
0310             'running_jobs': running_jobs,
0311             'running_tasks': running_tasks,
0312             'finished_12h': finished,
0313             'failed_12h': failed,
0314             'success_pct_12h': pct,
0315             'paused_tasks': paused_count,
0316             'paused_task_detail': paused_tasks,
0317         }
0318     except Exception as exc:
0319         logger.error('capcom state: panda queries failed: %s', exc)
0320         states.append({'source': 'swf-panda', 'value': 'UNAVAILABLE',
0321                        'url': f'{REMOTE_FACE}/panda/jobs/'})
0322         detail['panda'] = {'error_text': str(exc)}
0323 
0324     try:
0325         from ..alarms_data import active_event_count, alarm_configs
0326 
0327         counts = {}
0328         for cfg in alarm_configs():
0329             entry_id = cfg.get('entry_id') or ''
0330             if entry_id:
0331                 counts[cfg.get('name') or entry_id] = (
0332                     active_event_count(entry_id))
0333         active = sum(counts.values())
0334         states.append({
0335             'source': 'swf-alarms',
0336             'value': f'{active} active' if active else 'OK',
0337             'color': 'red' if active else 'green',
0338             'url': f'{REMOTE_FACE}/alarms/'})
0339         detail['alarms'] = {
0340             'active': active,
0341             'by_alarm': {name: n for name, n in counts.items() if n}}
0342     except Exception as exc:
0343         logger.error('capcom state: alarm counts failed: %s', exc)
0344         states.append({'source': 'swf-alarms', 'value': 'UNAVAILABLE',
0345                        'url': f'{REMOTE_FACE}/alarms/'})
0346         detail['alarms'] = {'error_text': str(exc)}
0347 
0348     try:
0349         from ..epicprod_logging import SUBLEVEL_VALUES, live_stream_q
0350         from ..models import AIMemory, AppLog, SysConfig
0351 
0352         since = now - timedelta(hours=24)
0353         # Posts: what the epicprod-live publisher put on the channel —
0354         # the same SysConfig-governed selection it publishes from.
0355         min_sublevel = str(SysConfig.get_setting(
0356             'epicprod_live_min_sublevel', 'normal') or '')
0357         if min_sublevel not in SUBLEVEL_VALUES:
0358             min_sublevel = 'normal'
0359         posts = AppLog.objects.filter(
0360             live_stream_q(min_sublevel), timestamp__gte=since).count()
0361         # Queries: DISpatcher-handled questions (channel, mentions, DMs)
0362         # — one user-role memory row is recorded per handled exchange.
0363         queries = AIMemory.objects.filter(
0364             username='pandabot', session_id='mattermost', role='user',
0365             created_at__gte=since).count()
0366         states.append({
0367             'source': 'swf-bot',
0368             'value': (f'{posts} post{"s" if posts != 1 else ""} · '
0369                       f'{queries} quer{"ies" if queries != 1 else "y"}/24h'),
0370             'url': 'https://chat.epic-eic.org/main/channels/dispatcher'})
0371         detail['dispatcher'] = {'posts_24h': posts, 'queries_24h': queries,
0372                                 'min_sublevel': min_sublevel}
0373     except Exception as exc:
0374         logger.error('capcom state: dispatcher counts failed: %s', exc)
0375         states.append({
0376             'source': 'swf-bot', 'value': 'UNAVAILABLE',
0377             'url': 'https://chat.epic-eic.org/main/channels/dispatcher'})
0378         detail['dispatcher'] = {'error_text': str(exc)}
0379 
0380     return JsonResponse({'built_at': now.isoformat(),
0381                          'states': states, 'detail': detail})
0382 
0383 
0384 def capcom_notices(request):
0385     """Open read: buffered discrete SWF events after a consumer's cursor.
0386 
0387     Query params:
0388         since (ISO-8601 timestamp, optional) — return notices created
0389         strictly after this instant; a naive value is read as UTC.
0390         Default: the trailing 24 hours.
0391 
0392     Rows come back oldest-first so the consumer's next cursor is the last
0393     row's created_at; 'more' is true when the page cap truncated the
0394     response and another read should follow immediately.
0395     """
0396     from ..models import CapcomNotice
0397 
0398     now = timezone.now()
0399     since_raw = (request.GET.get('since') or '').strip()
0400     if since_raw:
0401         try:
0402             since = datetime.fromisoformat(since_raw)
0403         except ValueError:
0404             return JsonResponse(
0405                 {'error': 'since must be an ISO-8601 timestamp'}, status=400)
0406         if timezone.is_naive(since):
0407             since = since.replace(tzinfo=datetime_timezone.utc)
0408     else:
0409         since = now - timedelta(hours=24)
0410 
0411     rows = list(CapcomNotice.objects.filter(created_at__gt=since)
0412                 .order_by('created_at')[:NOTICE_PAGE_MAX + 1])
0413     more = len(rows) > NOTICE_PAGE_MAX
0414     rows = rows[:NOTICE_PAGE_MAX]
0415     return JsonResponse({
0416         'built_at': now.isoformat(),
0417         'since': since.isoformat(),
0418         'more': more,
0419         'notices': [{
0420             'created_at': row.created_at.isoformat(),
0421             'source': row.source,
0422             'severity': row.severity,
0423             'title': row.title,
0424             'detail': row.detail,
0425             'url': row.url,
0426             'dedup_key': row.dedup_key,
0427         } for row in rows],
0428     })
0429 
0430 
0431 @api_view(['POST'])
0432 @authentication_classes([TokenAuthentication])
0433 @permission_classes([IsAuthenticated])
0434 def capcom_notice_ingest(request):
0435     """Token-authenticated notice write from the prod-ops agent."""
0436     from ..models import CapcomNotice
0437 
0438     source = str(request.data.get('source') or '').strip()
0439     title = str(request.data.get('title') or '').strip()
0440     if not source or not title:
0441         return Response({'error': 'source and title are required'},
0442                         status=400)
0443     notice = CapcomNotice.objects.create(
0444         source=source[:100],
0445         severity=str(request.data.get('severity') or 'info')[:20],
0446         title=title[:300],
0447         detail=str(request.data.get('detail') or ''),
0448         url=str(request.data.get('url') or '')[:500],
0449         dedup_key=str(request.data.get('dedup_key') or '')[:200],
0450     )
0451     purged, _ = CapcomNotice.objects.filter(
0452         created_at__lt=timezone.now()
0453         - timedelta(days=NOTICE_RETENTION_DAYS)).delete()
0454     if purged:
0455         logger.info('capcom notices: purged %d expired rows', purged)
0456     return Response({'status': 'ok', 'id': notice.id})
0457 
0458 
0459 def capcom_user_state(request):
0460     """One display-ready SWF tile for a requested user's own activity/state."""
0461     username = (request.GET.get('username') or '').strip()
0462     if not USERNAME_RE.fullmatch(username):
0463         return JsonResponse({
0464             'error': ('username is required and may contain only letters, '
0465                       'numbers, dot, underscore, and hyphen'),
0466         }, status=400)
0467 
0468     now = timezone.now()
0469     detail = {'username': username}
0470     color = None
0471     try:
0472         testbed = _user_testbed_summary(username, now)
0473         testbed_label = testbed.pop('label')
0474         color = testbed.pop('color')
0475         detail['testbed'] = testbed
0476     except Exception as exc:
0477         logger.error('capcom user state: testbed query for %s failed: %s',
0478                      username, exc)
0479         testbed_label = 'testbed unavailable'
0480         color = 'red'
0481         detail['testbed'] = {'error_text': str(exc)}
0482 
0483     try:
0484         panda = _user_panda_summary(username)
0485         panda_label = panda.pop('label')
0486         detail['panda'] = panda
0487     except Exception as exc:
0488         logger.error('capcom user state: PanDA query for %s failed: %s',
0489                      username, exc)
0490         panda_label = 'PanDA unavailable'
0491         color = 'red'
0492         detail['panda'] = {'error_text': str(exc)}
0493 
0494     entry = {
0495         'source': 'swf-user',
0496         'value': f'{testbed_label} · {panda_label}',
0497         'url': f'{REMOTE_FACE}/panda/jobs/?{urlencode({"days": 1, "username": username})}',
0498     }
0499     if color:
0500         entry['color'] = color
0501     return JsonResponse({
0502         'built_at': now.isoformat(),
0503         'username': username,
0504         'states': [entry],
0505         'detail': detail,
0506     })