Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 10:47:33

0001 """The comp snapshot: the computing centre room's live record
0002 (docs/COMP_SNAPSHOT.md).
0003 
0004 One builder over the PanDA and swf databases, served as the cached
0005 product ``comp_snapshot:v1`` (docs/CACHED_PRODUCTS.md) by REST at
0006 ``panda/comp/snapshot.json`` and by the MCP tool ``swf_comp_snapshot``.
0007 No remote call and no LLM: the build reads tables only. A part that
0008 cannot be read is listed in ``errors`` and left out, never shown as zero.
0009 """
0010 import json
0011 import logging
0012 import re
0013 import time
0014 from datetime import datetime, timedelta, timezone as dt_timezone
0015 from zoneinfo import ZoneInfo
0016 from urllib.parse import quote, urlencode
0017 
0018 from django.db import connections
0019 from django.utils import timezone
0020 
0021 from monitor_app.short_names import short_name, short_text
0022 from monitor_app import error_corrections
0023 from monitor_app.panda.constants import ERROR_COMPONENTS
0024 from monitor_app.panda.queries import INCOMPLETE_REGISTRATION_OUTCOMES
0025 from monitor_app.panda.error_labels import category_label
0026 from monitor_app.snapper_errors import _classify_sql
0027 
0028 logger = logging.getLogger(__name__)
0029 
0030 PRODUCT_KEY = 'comp_snapshot:v1'
0031 TTL_SECONDS = 60
0032 SCHEMA = 'comp-snapshot/1'
0033 PUBLIC = 'https://epic-devcloud.org/prod'
0034 
0035 # The room's site groups, seeded into SysConfig ``comp.sites`` on first
0036 # read so a site is added or regrouped without a deploy. Each site's queues
0037 # and their status are the queue registry's (swf-epicprod
0038 # docs/QUEUE_REGISTRY.md): a status there other than production overrides
0039 # what PanDA says, and a retired queue leaves the room.
0040 DEFAULT_SITES = [
0041     {'id': 'osg', 'name': 'OSG', 'execute_sites': True},
0042     {'id': 'grex', 'name': 'GREX'},
0043     {'id': 'compute-canada', 'name': 'Canada'},
0044     {'id': 'nersc', 'name': 'Perlmutter'},
0045     {'id': 'google', 'name': 'Google'},
0046     {'id': 'jlab', 'name': 'JLab'},
0047 ]
0048 INACTIVE_SITE_LABELS = {
0049     'nersc': 'No allocation',
0050     'compute-canada': 'Authentication block',
0051     'jlab': 'Cybersecurity block',
0052 }
0053 # A live site's standing note. None stands now: Perlmutter's nodes show in its
0054 # title row ("1 node x 128 cores", PROD_DASHBOARD.md).
0055 LIVE_SITE_LABELS = {}
0056 
0057 # GREX runs eight pilots per Slurm worker (2026-10-20).
0058 from swf_epicprod.regulation import PILOTS_PER_WORKER
0059 from swf_epicprod import perlmutter_production
0060 OSG_SUBMIT_HOST = 'osgsub01'
0061 # The harvester reporters (docs/HARVESTER_REPORTER.md); each record names
0062 # the queues its harvester serves. osgsub01's own key is the OSG submit
0063 # reporter's, which carries the schedd's idle pilots per queue.
0064 from monitor_app.host_reports import HARVESTER_HOSTS as HARVESTER_REPORTS  # noqa: E402
0065 # The reporters run every five minutes; three missed runs make a record stale.
0066 REPORT_STALE_SECONDS = 900
0067 # How old a harvester's job-demand cache may be before its pilot supply is
0068 # in doubt; None where the record cannot carry it (the Perlmutter record is
0069 # built from the PanDA server, which the harvester's cache never reaches).
0070 DEMAND_MAX_AGE_SECONDS = {'osgsub01-harvester': 90, 'perlmutter-harvester': None}
0071 LEVELS = ('grey', 'green', 'amber', 'red')
0072 # The pilot's log stage-out error codes.
0073 LOG_STAGEOUT_CODES = (1137, 1151, 1152)
0074 # The registration outcomes that leave a file owed to JLab Rucio: pending
0075 # (the registrar completes it) and unfinished. ``diverted`` is a clash
0076 # registered under a derived name (swf-epicprod RUCIO_RESILIENCE.md,
0077 # Measure 3), so it is registered.
0078 REGISTRATION_OWED = INCOMPLETE_REGISTRATION_OUTCOMES
0079 
0080 # The production output RSE: every production configuration names it as
0081 # OUT_RSE (swf-epicprod RUCIO_FAILOVER_STASH.md).
0082 OUTPUT_RSE = 'BNL-XRD'
0083 # A daily assessment's verdict as a health level (swf-epicprod
0084 # EPICPROD_ASSESSMENTS.md: ok | attention | alarm).
0085 VERDICT_LEVELS = {'ok': 'green', 'attention': 'amber', 'alarm': 'red'}
0086 # The epicprod-live screen: warning and error records, every one the rules
0087 # keep; the panel grows downward (Torre Wenaus, 2026-10-29: "panel should
0088 # show what it is supposed to show. expands down").
0089 LIVE_LEVELS = ('WARNING', 'ERROR', 'CRITICAL')
0090 # The floor's trend plots: twelve hours (Torre, 2026-10-22) in quarter-hour
0091 # bins, fine enough to show a ramp or a dip.
0092 TREND_HOURS = 12
0093 TREND_BIN_MINUTES = 15
0094 # A state sample older than this before a bin end is not the state then: the
0095 # PanDA reading refreshes every five minutes.
0096 TREND_STALE_MINUTES = 15
0097 # The JLab output flow's kept bins, and how many before the newest kept one
0098 # each build reads again (jobs reach the archive shortly after they end).
0099 JLAB_FLOW_KEY = 'jlab_flow:v1'
0100 JLAB_FLOW_REREAD_BINS = 2
0101 JLAB_FLOW_BACKFILL_BINS = 4
0102 # The floor is the control room: live-now records only, the last 12 hours;
0103 # the logs page holds the history (Torre, 2026-10-22).
0104 LIVE_WINDOW_H = 12
0105 # A task Move is normal operation, shown at INFO for this long (SysConfig
0106 # comp.live.move_hours; Torre Wenaus, 2026-10-29: "make it 3hrs").
0107 LIVE_MOVE_HOURS = 3
0108 
0109 
0110 def _worst(levels):
0111     levels = [lv for lv in levels if lv in LEVELS]
0112     if not levels:
0113         return 'grey'
0114     real = [lv for lv in levels if lv != 'grey']
0115     return max(real, key=LEVELS.index) if real else 'grey'
0116 
0117 
0118 def _health(parts):
0119     return {'level': _worst(p['level'] for p in parts.values()), 'parts': parts}
0120 
0121 
0122 def _part(level, value=None, note=''):
0123     return {'level': level, 'value': value, 'note': note}
0124 
0125 
0126 def _activity_part(finished, failed):
0127     ended = finished + failed
0128     if ended == 0:
0129         return _part('grey', 0, 'no jobs ended in the last hour')
0130     if finished == 0 and failed >= 10:
0131         level = 'red'
0132     elif failed * 4 > ended:
0133         level = 'amber'
0134     else:
0135         level = 'green'
0136     return _part(level, round(failed / ended, 3),
0137                  f'{finished} finished, {failed} failed in the last hour')
0138 
0139 
0140 def _fixed_limit(config):
0141     """Whether harvester applies the queue's nQueueLimitWorker unchanged:
0142     a launch-limits row whose ratio is unset or zero."""
0143     ratio = (config or {}).get('n_queue_limit_worker_ratio')
0144     try:
0145         return ratio is None or int(ratio) == 0
0146     except (ValueError, TypeError):
0147         return False
0148 
0149 
0150 def effective_waiting_limit(config, applied, now):
0151     """Use the fixed DB limit or the last applied value carried by the reporter.
0152 
0153     The limit is fixed when no ratio is set or the ratio is zero: harvester
0154     then applies nQueueLimitWorker as it stands (its db_proxy
0155     get_worker_limits). The adjuster skips its limit log when the waiting
0156     queue is already full. Its timestamp records an event, not a heartbeat:
0157     freshness belongs to the enclosing report, checked separately by
0158     ``_pilot_supply_part``.
0159     """
0160     config = config or {}
0161     try:
0162         if _fixed_limit(config):
0163             return int(config['n_queue_limit_worker'])
0164     except (KeyError, ValueError, TypeError):
0165         pass
0166     try:
0167         at = datetime.fromisoformat(applied['at'].replace('Z', '+00:00'))
0168         limit = int(applied['queue_limit'])
0169         if at <= now and limit >= 0:
0170             return limit
0171     except (KeyError, ValueError, TypeError):
0172         pass
0173     return None
0174 
0175 
0176 def _pilot_display(pilots):
0177     """Resolve the waiting count once while preserving both raw readings."""
0178     if not isinstance(pilots, dict):
0179         return pilots
0180     schedd = pilots.get('waiting_schedd')
0181     waiting = schedd if schedd is not None else pilots.get('waiting')
0182     return dict(pilots, waiting_current=waiting,
0183                 waiting_source=('schedd' if schedd is not None else
0184                                 'harvester' if waiting is not None else None))
0185 
0186 
0187 def _node_ceilings():
0188     """{queue: the most nodes it may hold}, SysConfig ``comp.node_ceilings``:
0189     Perlmutter's Event Service queue at five (Torre Wenaus, 2026-10-28)."""
0190     from monitor_app.models import SysConfig
0191     value = SysConfig.get_setting('comp.node_ceilings', {'NERSC_Perlmutter_epic_es': 5})
0192     return value if isinstance(value, dict) else {}
0193 
0194 
0195 def _worker_limit(cfg, queue):
0196     """The operator's active worker limit control on ``queue``
0197     (``pilot.queue.<queue>.worker_limit``), or None."""
0198     control = cfg.get(f'pilot.queue.{queue}.worker_limit') or {}
0199     if (isinstance(control, dict) and control.get('active')
0200             and isinstance(control.get('limit'), int) and control['limit'] > 0):
0201         return control
0202     return None
0203 
0204 
0205 def _pilot_supply_part(p, queued, started_1h, submitted_1h, workers=None, operator_limit=None):
0206     """Whether pilots keep reaching the queue (docs/COMP_SNAPSHOT.md, Pilot
0207     supply). Harvester holds new pilots back against its own count of
0208     those waiting, so a count the schedd no longer bears out stops supply
0209     while the queue reads full. ``workers`` (running, submitted) is PanDA's
0210     own worker table, read for a queue no harvester report names. A queue
0211     whose running slots fill the operator's worker limit is due no pilot:
0212     the limit is its own line (Torre Wenaus, 2026-10-28, GREX full at its
0213     300-core limit read red here)."""
0214     if not p:
0215         running, submitted = workers or (0, 0)
0216         if running or submitted:
0217             return _part('green', submitted, f'no harvester report; PanDA records {running} worker(s) '
0218                                              f'running and {submitted} submitted')
0219         return _part('amber' if queued else 'grey', None, 'no harvester report names the queue, '
0220                                                           'and PanDA records no worker running or submitted')
0221     age = p.get('report_age_seconds')
0222     stale_seconds = p.get('report_stale_seconds', REPORT_STALE_SECONDS)
0223     if age is None or age > stale_seconds:
0224         return _part('amber' if queued else 'grey', None, f"harvester report {int((age or 0) // 60)} min old")
0225     if p.get('error'):
0226         return _part('amber' if queued else 'grey', None, p['error'])
0227     demand_age = p.get('demand_age_seconds')
0228     if queued and p.get('demand_max_age_seconds') is not None and (
0229             demand_age is None or demand_age > p['demand_max_age_seconds']):
0230         return _part('amber', demand_age, 'job-demand cache missing or stale')
0231     waiting, limit = p['waiting'], p.get('limit')
0232     if operator_limit and (p.get('running') or 0) >= operator_limit:
0233         return _part('green', p['running'], f"{p['running']} of the operator's limit of "
0234                                            f"{operator_limit} slots running: full, no pilot due")
0235     try:
0236         ceiling = int(p.get('max_workers'))
0237     except (TypeError, ValueError):
0238         ceiling = None
0239     if ceiling and (p.get('running') or 0) >= ceiling:
0240         # harvester starts no worker past its maxWorkers (Perlmutter's node
0241         # ceiling, NERSC_PERLMUTTER.md), so a full queue is due no pilot
0242         return _part('green', p['running'], f"{p['running']} of harvester's {ceiling} workers "
0243                                            f"running: full, no pilot due")
0244     if queued and not waiting and not started_1h and not submitted_1h:
0245         return _part('red', 0, f"{queued} jobs queued, no pilot waiting, submitted "
0246                                f"or started in the last hour")
0247     if limit is None:
0248         return _part('amber' if queued else 'grey', waiting, f"harvester counts {waiting} waiting; the limit "
0249                                       f"it applies is not reported")
0250     label = 'last applied limit' if p.get('limit_source') == 'adjuster' else 'limit'
0251     seen = f"harvester counts {waiting} waiting (its {label} {limit})"
0252     idle = p.get('waiting_schedd')
0253     if idle is not None and (p.get('schedd_age_seconds') or 0) <= stale_seconds:
0254         seen += f", the schedd holds {idle}"
0255         if waiting >= 2 * limit and idle < limit:
0256             return _part('red' if queued else 'amber', waiting - idle,
0257                          f"{seen}: harvester counts pilots the schedd no longer has "
0258                          f"and submits none")
0259         # what the pool can start: the pilots the schedd holds idle and
0260         # those harvester will still submit before its count reaches its limit
0261         startable = idle + max(limit - waiting, 0)
0262         if startable < limit / 2:
0263             return _part('red' if queued else 'amber', startable,
0264                          f"{seen}: harvester will add {max(limit - waiting, 0)}, so the "
0265                          f"pool can start {startable}, under half the limit; harvester "
0266                          f"has not yet seen its waiting pilots start")
0267     return _part('green', waiting, seen)
0268 
0269 
0270 def _aware(t):
0271     """A PanDA time (naive UTC) made aware."""
0272     return timezone.make_aware(t, dt_timezone.utc) if t is not None and timezone.is_naive(t) else t
0273 
0274 
0275 def _rows(sql, params=()):
0276     with connections['panda'].cursor() as cursor:
0277         cursor.execute(sql, params)
0278         return cursor.fetchall()
0279 
0280 
0281 # A PanDA task status after which the task delivers nothing more.
0282 TASK_ENDED = ('done', 'finished', 'failed', 'broken', 'aborted', 'exhausted')
0283 
0284 # PanDA keeps a job's destinationsite in 36 characters, so a longer
0285 # GLIDEIN_Site arrives cut ('Center for Advanced Research Computi').
0286 PANDA_SITE_CHARS = 36
0287 
0288 
0289 def _uncut_site(name):
0290     """An execute site's full name: one PanDA cut at its 36 characters is
0291     restored from the full names the short-name table knows; any other
0292     name passes unchanged."""
0293     from monitor_app.short_names import SHORT_NAMES
0294     if not name or len(name) != PANDA_SITE_CHARS:
0295         return name
0296     full = [n for n in SHORT_NAMES if len(n) > PANDA_SITE_CHARS and n.startswith(name)]
0297     return full[0] if len(full) == 1 else name
0298 
0299 
0300 def _task_pcs(tasks):
0301     """Each PanDA task's physics configuration as PCS records it:
0302     ``{jeditaskid: (pc label, one-line physics)}``, the line PCS's summary
0303     (process, beams, Q², generator, sample, background, requesters).
0304     ``tasks`` is a set of (jeditaskid, taskname); a task PCS links nowhere
0305     is found by the composed identity its name carries, and one PCS does
0306     not know is left out. Local reads only."""
0307     try:
0308         from pcs.models import Dataset, PandaTasks
0309         from pcs.name_tokens import logical_name_from_physical_name
0310         from pcs.physics_config import summary
0311     except ImportError:
0312         return {}
0313     related = ('physics_config__physics_tag', 'physics_config__background_tag')
0314     out = {}
0315     for link in (PandaTasks.objects.filter(jedi_task_id__in={t for t, _ in tasks})
0316                  .select_related(*(f'prod_task__dataset__{r}' for r in related))):
0317         ds = link.prod_task.dataset if link.prod_task.dataset_id else None
0318         if ds is not None and ds.physics_config_id:
0319             out[link.jedi_task_id] = (ds.physics_config.label, summary(ds.physics_config))
0320     for tid, name in tasks:
0321         if tid in out:
0322             continue
0323         ds = (Dataset.objects.filter(composed_name=logical_name_from_physical_name(name or ''))
0324               .select_related(*related).first())
0325         if ds is not None and ds.physics_config_id:
0326             out[tid] = (ds.physics_config.label, summary(ds.physics_config))
0327     return out
0328 
0329 
0330 def _task_physics(tasks):
0331     """What each PanDA task simulates, its tags read as physics:
0332     ``{jeditaskid: one-line physics}`` (_task_pcs)."""
0333     return {tid: line for tid, (_pc, line) in _task_pcs(tasks).items()}
0334 
0335 
0336 def _physics_label(line):
0337     """The physics in a row's width: PCS's summary without its tail
0338     (generator, sample, requesters), the nucleus its beam species already
0339     names, and the units ('DIS Au197 eAu 9x100 GeV Q² 10–100 · beagle ...'
0340     -> 'DIS eAu 9x100 Q² 10–100')."""
0341     import re
0342     head = (line or '').split(' · ', 1)[0].replace('_', ' ').replace(' GeV', '')
0343     return re.sub(r'\b[A-Z][a-z]?\d{1,3}\s(?=e[A-Z])', '', head).strip()
0344 
0345 
0346 def _plan_progress():
0347     """The campaign plan's delivered and target events per physics
0348     configuration, from the stored completion record
0349     (``prod_hub_campaign_completion``) as it stands: never built here."""
0350     from monitor_app.models import CachedProduct
0351     row = CachedProduct.objects.filter(key='prod_hub_campaign_completion').values('value').first()
0352     out = {}
0353     for entry in ((row or {}).get('value') or []):
0354         for pc in entry.get('pcs') or []:
0355             if pc.get('pc'):
0356                 out[pc['pc']] = {'target': pc.get('target'),
0357                                  'delivered_events': pc.get('delivered_events')}
0358     return out
0359 
0360 
0361 class _Build:
0362     def __init__(self):
0363         from monitor_app.panda.census import PANDA_SCHEMA
0364         self.S = PANDA_SCHEMA
0365         self.now = timezone.now()
0366         self.hour_ago = self.now - timedelta(hours=1)
0367         self.errors = []
0368 
0369     def part(self, name, fn, fallback=None):
0370         try:
0371             return fn()
0372         except Exception as exc:  # noqa: BLE001
0373             logger.exception('comp snapshot: %s failed', name)
0374             self.errors.append({'part': name, 'error': f'{type(exc).__name__}: {exc}'})
0375             return fallback
0376 
0377     # -- readings -------------------------------------------------------
0378 
0379     def ended(self, column):
0380         """Production jobs ended in the last hour by ``column``: counts and
0381         core-hours, finished and failed."""
0382         out = {}
0383         sql = f'''
0384             SELECT {column}, jobstatus, COUNT(*),
0385                    SUM(GREATEST(COALESCE(corecount, 1), 1)
0386                        * EXTRACT(EPOCH FROM (endtime - starttime))) / 3600.0
0387             FROM (
0388               SELECT {column}, jobstatus, corecount, endtime, starttime FROM "{self.S}"."jobsactive4"
0389                WHERE endtime >= %s AND processingtype = 'epicproduction'
0390                  AND jobstatus IN ('finished', 'failed')
0391               UNION ALL
0392               SELECT {column}, jobstatus, corecount, endtime, starttime FROM "{self.S}"."jobsarchived4"
0393                WHERE endtime >= %s AND processingtype = 'epicproduction'
0394                  AND jobstatus IN ('finished', 'failed')) t
0395             GROUP BY 1, 2'''
0396         for key, status, n, core_h in _rows(sql, (self.hour_ago, self.hour_ago)):
0397             entry = out.setdefault(key or '', {'finished': 0, 'failed': 0,
0398                                                'finished_core_h': 0.0, 'failed_core_h': 0.0})
0399             entry[status] = int(n)
0400             entry[f'{status}_core_h'] = round(float(core_h or 0), 1)
0401         return out
0402 
0403     def top_failures(self):
0404         """The last hour's failures per queue by Snapper error category,
0405         largest first: component x code, the job's first nonzero error
0406         component in ERROR_COMPONENTS order (SNAPPER_ERRORS.md), labelled
0407         from the code catalogs and read through the error-correction root
0408         (ERROR_ATTRIBUTION.md), so a label a rule marks unreliable shows
0409         its corrected reading. A failed job with no error code in any of
0410         the seven components reads as such; nothing reads "unattributed"
0411         (Torre, 2026-10-23: the OSG failures of 41039 and 41040 were DDM
0412         200, the log-name collision, and the room called them that)."""
0413         comp_case, code_case, diag_case = _classify_sql()
0414         fields = ', '.join(f'"{c["code"]}", "{c["diag"]}"' for c in ERROR_COMPONENTS)
0415         sql = f'''
0416             SELECT computingsite, component, code, MIN(diag), transexitcode, COUNT(*) FROM (
0417               SELECT computingsite, transexitcode,
0418                      CASE {comp_case} ELSE 'none' END AS component,
0419                      CASE {code_case} ELSE 0 END AS code,
0420                      CASE {diag_case} ELSE '' END AS diag
0421                 FROM (
0422                   SELECT computingsite, transexitcode, {fields} FROM "{self.S}"."jobsactive4"
0423                    WHERE endtime >= %s AND processingtype = 'epicproduction' AND jobstatus = 'failed'
0424                   UNION ALL
0425                   SELECT computingsite, transexitcode, {fields} FROM "{self.S}"."jobsarchived4"
0426                    WHERE endtime >= %s AND processingtype = 'epicproduction' AND jobstatus = 'failed') j) t
0427             GROUP BY 1, 2, 3, 5'''
0428         cats = {}
0429         for site, component, code, diag, exitcode, n in _rows(sql, (self.hour_ago, self.hour_ago)):
0430             cat = cats.setdefault((site, component, int(code or 0)),
0431                                   {'n': 0, 'diag': diag or '', 'exits': {}})
0432             cat['n'] += int(n)
0433             if exitcode not in (None, ''):
0434                 cat['exits'][str(exitcode)] = cat['exits'].get(str(exitcode), 0) + int(n)
0435         out = {}
0436         for (site, component, code), cat in cats.items():
0437             if component == 'none':
0438                 label = 'failed with no error code in any component'
0439             else:
0440                 label = category_label(component, code)
0441                 try:
0442                     rule = error_corrections.match(component, code, cat['diag'], sites=[site])
0443                     if rule is not None:
0444                         label = error_corrections.correction(rule, cat['exits'])['label']
0445                 except Exception as e:  # a correction must never take the room down
0446                     logger.error('error-correction read failed for %s %s: %s', component, code, e)
0447             out.setdefault(site, []).append((label, cat['n']))
0448         for site in out:
0449             out[site].sort(key=lambda t: -t[1])
0450         return out
0451 
0452     def production_alarms(self):
0453         """The active alarm events that concern production: a task's
0454         failure-rate alarm only when the task is a production task
0455         (processingtype epicproduction). The failure-rate alarms watch
0456         every EIC task, a user's included, and the room reports
0457         production."""
0458         alarms = _active_alarms()
0459         ids = {int(m.group(1)) for m in (TASK_ALARM_RE.match(a['title']) for a in alarms) if m}
0460         types = {}
0461         if ids:
0462             sql = f'''SELECT jeditaskid, processingtype FROM "{self.S}"."jedi_tasks"
0463                       WHERE jeditaskid = ANY(%s)'''
0464             types = {int(t): p for t, p in _rows(sql, (list(ids),))}
0465         out = []
0466         for a in alarms:
0467             m = TASK_ALARM_RE.match(a['title'])
0468             if m and types.get(int(m.group(1))) != 'epicproduction':
0469                 continue
0470             out.append(a)
0471         return out
0472 
0473     def floor_tasks(self, site_name, alarms, site_of):
0474         """The task board (COMP_SNAPSHOT.md, Tasks): every production task
0475         with jobs running, starting or queued at the floor's queues, newest
0476         first; a task with none there is off the board."""
0477         from monitor_app.panda.census import (NOT_STARTED, RUNNING,
0478                                               TASK_TERMINAL_STATUSES,
0479                                               ungenerated_by_task)
0480         from pcs.models import Dataset
0481         from pcs.views import physics_description
0482         terminal = list(TASK_TERMINAL_STATUSES)
0483         sql = f'''
0484             SELECT a.jeditaskid, t.taskname, t.status,
0485                    COALESCE(t.starttime, t.creationdate),
0486                    SUM(CASE WHEN a.jobstatus = ANY(%s) THEN 1 ELSE 0 END), COUNT(*),
0487                    MODE() WITHIN GROUP (ORDER BY a.computingsite),
0488                    ARRAY_AGG(DISTINCT a.computingsite)
0489             FROM "{self.S}"."jobsactive4" a
0490             JOIN "{self.S}"."jedi_tasks" t ON t.jeditaskid = a.jeditaskid
0491             WHERE a.jobstatus = ANY(%s)
0492               AND t.processingtype = 'epicproduction'
0493               AND (t.status IS NULL OR NOT t.status = ANY(%s))
0494             GROUP BY a.jeditaskid, t.taskname, t.status, t.starttime, t.creationdate'''
0495         # Every production task in flight is on the board wherever it is
0496         # placed, by the same rules as any other (Torre Wenaus, 2026-10-22,
0497         # when two tasks sat on a queue taken off the floor).
0498         rows = _rows(sql, (list(RUNNING), list(NOT_STARTED + RUNNING), terminal))
0499         # A task with rows JEDI has not yet made into jobs is on the board
0500         # though none of its jobs waits or runs now.
0501         ungenerated = {tid: (queue, n) for tid, (queue, n) in ungenerated_by_task().items()
0502                        if n > 0}
0503         to_generate = {tid: n for tid, (_queue, n) in ungenerated.items()}
0504         waiting = sorted(set(to_generate) - {int(r[0]) for r in rows})
0505         if waiting:
0506             sql = f'''
0507                 SELECT jeditaskid, taskname, status, COALESCE(starttime, creationdate),
0508                        0, 0, site, NULL
0509                 FROM "{self.S}"."jedi_tasks" WHERE jeditaskid = ANY(%s)'''
0510             rows = list(rows) + list(_rows(sql, (waiting,)))
0511         ids = [int(r[0]) for r in rows]
0512         files = {}
0513         if ids:
0514             sql = f'''SELECT jeditaskid, SUM(nfiles), SUM(nfilesfinished), SUM(nfilesfailed)
0515                       FROM "{self.S}"."jedi_datasets"
0516                       WHERE jeditaskid = ANY(%s) AND masterid IS NULL
0517                         AND type IN ('input', 'pseudo_input')
0518                       GROUP BY jeditaskid'''
0519             files = {int(t): (int(n or 0), int(f or 0), int(x or 0))
0520                      for t, n, f, x in _rows(sql, (ids,))}
0521         alarmed = {int(m.group(1)) for m in (TASK_ALARM_RE.match(a['title']) for a in alarms) if m}
0522         names = {r[1]: re.sub(r'\.try\d+$', '', r[1]) for r in rows}
0523         datasets = {d.composed_name: d for d in
0524                     Dataset.objects.filter(composed_name__in=set(names.values()))
0525                     .select_related('physics_tag', 'evgen_tag')
0526                     .order_by('composed_name', 'block_num').distinct('composed_name')}
0527         items = []
0528         for tid, name, status, at, running, active, queue, queues in sorted(
0529                 rows, key=lambda r: r[3], reverse=True):
0530             tid, running, active = int(tid), int(running or 0), int(active or 0)
0531             # The floor's queues the task has jobs at, and the queue of its
0532             # rows still to generate.
0533             queues = sorted(set(queues or ())
0534                             | ({ungenerated[tid][0]} if tid in ungenerated else set()))
0535             if at is not None and at.tzinfo is None:
0536                 at = at.replace(tzinfo=dt_timezone.utc)  # PanDA stores UTC
0537             n, done, lost = files.get(tid, (0, 0, 0))
0538             d = datasets.get(names[name])
0539             physics = physics_description(d, concise=True) if d else ''
0540             lost_pct = 100.0 * lost / (done + lost) if done + lost else 0.0
0541             local = at.astimezone(ZoneInfo('America/New_York')) if at else None
0542             detail = (' at '.join(x for x in (physics, site_name.get(queue, queue)) if x)
0543                       + f". {name}, {status} on {short_name(queue)}"
0544                       + (f" since {local:%m-%d %H:%M} ET" if local else '')
0545                       + f"; {done:,} of {n:,} files done, {lost:,} lost after retries"
0546                       + (f" ({lost_pct:.1f}%), in alarm" if tid in alarmed else '') + '.')
0547             items.append({
0548                 'id': tid, 'at': at.isoformat() if at else None,
0549                 'level': ('red' if tid in alarmed else 'amber' if lost
0550                           else 'green' if running else 'grey'),
0551                 'title': str(tid), 'name': name,
0552                 'counts': {'ungenerated': to_generate.get(tid, 0),
0553                            'queued': active - running, 'running': running,
0554                            'finished': done, 'failed': lost},
0555                 'detail': detail,
0556                 'sites': sorted({site_of[q] for q in queues if q in site_of}),
0557                 'queues': queues,
0558                 'link': f'{PUBLIC}/panda/tasks/{tid}/'})
0559         return {'link': f'{PUBLIC}/panda/tasks/?processingtype=epicproduction',
0560                 'items': items}
0561 
0562     def paused_tasks(self, site_name):
0563         """The paused pillar (COMP_SNAPSHOT.md, Paused): every production
0564         task paused in PanDA, wherever it is placed, oldest pause first.
0565         The pause time is the task's last state change."""
0566         sql = f'''SELECT t.jeditaskid, t.taskname, t.site,
0567                          COALESCE(t.statechangetime, t.modificationtime),
0568                          SUM(d.nfiles), SUM(d.nfilesfinished), SUM(d.nfilesfailed)
0569                   FROM "{self.S}"."jedi_tasks" t
0570                   LEFT JOIN "{self.S}"."jedi_datasets" d ON d.jeditaskid = t.jeditaskid
0571                        AND d.masterid IS NULL AND d.type IN ('input', 'pseudo_input')
0572                   WHERE t.status = 'paused' AND t.processingtype = 'epicproduction'
0573                   GROUP BY 1, 2, 3, 4 ORDER BY 4, 1'''
0574         from swf_epicprod.quiet_end import quiet_task_ids
0575         quiet = quiet_task_ids()        # ended quietly: never on the floor (EPICPROD_OPS.md)
0576         items, oldest = [], None
0577         for tid, name, queue, since, n, done, lost in _rows(sql, ()):
0578             if int(tid) in quiet:
0579                 continue
0580             if since is not None and since.tzinfo is None:
0581                 since = since.replace(tzinfo=dt_timezone.utc)  # PanDA stores UTC
0582             oldest = oldest or since
0583             local = since.astimezone(ZoneInfo('America/New_York')) if since else None
0584             n, done, lost = int(n or 0), int(done or 0), int(lost or 0)
0585             where = site_name.get(queue, queue) or 'no queue'
0586             items.append({
0587                 'id': int(tid), 'name': name, 'queue': queue or '',
0588                 'since': since.isoformat() if since else None,
0589                 'link': f'{PUBLIC}/panda/tasks/{int(tid)}/',
0590                 'detail': (f"{int(tid)} paused on {where}"
0591                            + (f" ({queue})" if queue and queue != where else '')
0592                            + (f" since {local:%m-%d %H:%M} ET" if local else '')
0593                            + f"; {done:,} of {n:,} files done, {lost:,} lost. {name}")})
0594         # Who paused each and why, where our tools paused it.
0595         accounts = _pause_accounts([i['id'] for i in items])
0596         for i in items:
0597             i['paused_by'], i['pause_reason'] = accounts.get(i['id'], (None, None))
0598         # The task list's window reaches back to the oldest pause.
0599         days = max(7, (self.now - oldest).days + 1) if oldest else 7
0600         listed = ', '.join(f"{i['id']} on {site_name.get(i['queue'], i['queue'])}"
0601                            + (f" since {datetime.fromisoformat(i['since']).astimezone(ZoneInfo('America/New_York')):%m-%d %H:%M} ET"
0602                               if i['since'] else '')
0603                            for i in items)
0604         return {'tasks': len(items),
0605                 'link': f'{PUBLIC}/panda/tasks/?status=paused&processingtype=epicproduction&days={days}',
0606                 'detail': (f"{len(items)} task{'s' if len(items) != 1 else ''} paused: {listed}"
0607                            if items else 'no production task is paused'),
0608                 'items': items}
0609 
0610     def pc_arrivals(self, site_name, hours=24):
0611         """Science arriving at JLab over the last ``hours`` (24, as the
0612         completed tasks; Torre Wenaus, 2026-10-24), per physics
0613         configuration (COMP_SNAPSHOT.md, Arrivals): the RECO files the
0614         finished production jobs registered and their events, from each
0615         job's payload report, grouped by task and the task's PC. Newest
0616         arrival first. Each PC carries its physics in words and the plan's
0617         delivered and target events, read from the stored completion
0618         record without building it."""
0619         since = (self.now - timedelta(hours=hours)).astimezone(dt_timezone.utc).replace(tzinfo=None)
0620         sql = f'''SELECT j.jeditaskid, t.taskname, t.status, j.computingsite, COUNT(*),
0621                          SUM(COALESCE((m.metadata::jsonb -> 'payload' -> 'outputs' -> 'reco' ->> 'events')::bigint, 0)),
0622                          MIN(j.endtime), MAX(j.endtime)
0623                   FROM "{self.S}"."jobsarchived4" j
0624                   JOIN "{self.S}"."jedi_tasks" t ON t.jeditaskid = j.jeditaskid
0625                   LEFT JOIN "{self.S}"."metatable" m ON m.pandaid = j.pandaid
0626                   WHERE j.endtime >= %s AND j.processingtype = 'epicproduction'
0627                     AND j.jobstatus = 'finished' AND j.jobmetrics LIKE %s
0628                   GROUP BY 1, 2, 3, 4'''
0629         rows = _rows(sql, (since, '%payloadRegistration=registered%'))
0630         pcs = _task_pcs({(int(r[0]), r[1]) for r in rows})
0631         progress = _plan_progress()
0632         by_pc = {}
0633         for tid, name, status, queue, files, events, first, last in rows:
0634             pc, physics = pcs.get(int(tid), (None, ''))
0635             key = pc or f'task {int(tid)}'
0636             b = by_pc.setdefault(key, {'pc': pc, 'physics': physics, 'files': 0, 'events': 0,
0637                                        'tasks': set(), 'sites': set(), 'first': None, 'last': None,
0638                                        'running': False})
0639             # A configuration is finished when every task delivering to it
0640             # in the window has ended (Torre Wenaus, 2026-10-24: green on
0641             # the dashboard is for finished).
0642             b['running'] = b['running'] or status not in TASK_ENDED
0643             b['files'] += int(files or 0)
0644             b['events'] += int(events or 0)
0645             b['tasks'].add(int(tid))
0646             b['sites'].add(site_name.get(queue, queue) or queue or '')
0647             first, last = _aware(first), _aware(last)
0648             b['first'] = min(x for x in (b['first'], first) if x) if (b['first'] or first) else None
0649             b['last'] = max(x for x in (b['last'], last) if x) if (b['last'] or last) else None
0650         et = ZoneInfo('America/New_York')
0651         items = []
0652         for key, b in by_pc.items():
0653             plan = progress.get(b['pc']) or {}
0654             delivered, target = plan.get('delivered_events'), plan.get('target')
0655             # Finished for this view (Torre Wenaus, 2026-10-24): every task
0656             # delivering in the window has ended, or the target is met; past
0657             # the target is met, not a fault, and the detail says by how much.
0658             met = bool(target) and delivered is not None and delivered >= target
0659             finished = met or not b['running']
0660             sites = ', '.join(sorted(s for s in b['sites'] if s))
0661             items.append({
0662                 'pc': b['pc'] or '', 'physics': b['physics'], 'label': _physics_label(b['physics']),
0663                 'state': 'finished' if finished else 'running',
0664                 'files': b['files'],
0665                 'events': b['events'], 'tasks': sorted(b['tasks']), 'sites': sorted(s for s in b['sites'] if s),
0666                 'first_at': b['first'].isoformat() if b['first'] else None,
0667                 'last_at': b['last'].isoformat() if b['last'] else None,
0668                 'delivered_events': delivered, 'target_events': target,
0669                 'link': (f"{PUBLIC}/pcs/config/{b['pc']}/" if b['pc']
0670                          else f"{PUBLIC}/panda/tasks/{min(b['tasks'])}/"),
0671                 'detail': (f"{b['pc'] or key}: {b['physics'] or 'physics not recorded in PCS'}. "
0672                            f"{b['files']:,} RECO file{'s' if b['files'] != 1 else ''}, "
0673                            f"{b['events']:,} events in {hours} h"
0674                            + (f", at {b['last'].astimezone(et):%H:%M} ET"
0675                               if b['first'] and b['first'] == b['last'] else
0676                               f", {b['first'].astimezone(et):%H:%M} to {b['last'].astimezone(et):%H:%M} ET"
0677                               if b['first'] and b['last'] else '')
0678                            + f", from task{'s' if len(b['tasks']) != 1 else ''} "
0679                            + ', '.join(str(t) for t in sorted(b['tasks']))
0680                            + (f" at {sites}" if sites else '') + '.'
0681                            + (f" Delivered {delivered:,} of {target:,} events ({100 * delivered / target:.0f}%)."
0682                               if delivered is not None and target else '')
0683                            + (' Target met.' if met else ' Finished.' if finished else ' Still running.'))})
0684         items.sort(key=lambda i: i['last_at'] or '', reverse=True)
0685         files = sum(i['files'] for i in items)
0686         events = sum(i['events'] for i in items)
0687         return {'pcs': len(items), 'files': files, 'events': events, 'window_hours': hours,
0688                 'link': f'{PUBLIC}/snapper/epicprod/campaign/',
0689                 'detail': (f"{len(items)} physics configuration{'s' if len(items) != 1 else ''} "
0690                            f"received {files:,} RECO files, {events:,} events at JLab in {hours} h"
0691                            if items else f'no science arrived at JLab in {hours} h'),
0692                 'items': items}
0693 
0694     def completed_tasks(self, site_name, hours=24):
0695         """The production tasks that completed in the last ``hours``
0696         (COMP_SNAPSHOT.md, Completed): done (every input file finished) or
0697         finished (some lost), newest first, the completion time the
0698         task's end time."""
0699         since = self.now - timedelta(hours=hours)
0700         sql = f'''SELECT t.jeditaskid, t.taskname, t.site, t.status,
0701                          COALESCE(t.endtime, t.statechangetime),
0702                          SUM(d.nfiles), SUM(d.nfilesfinished), SUM(d.nfilesfailed)
0703                   FROM "{self.S}"."jedi_tasks" t
0704                   LEFT JOIN "{self.S}"."jedi_datasets" d ON d.jeditaskid = t.jeditaskid
0705                        AND d.masterid IS NULL AND d.type IN ('input', 'pseudo_input')
0706                   WHERE t.status IN ('done', 'finished') AND t.processingtype = 'epicproduction'
0707                     AND COALESCE(t.endtime, t.statechangetime) >= %s
0708                   GROUP BY 1, 2, 3, 4, 5 ORDER BY 5 DESC, 1 DESC'''
0709         items = []
0710         rows = _rows(sql, (since.astimezone(dt_timezone.utc).replace(tzinfo=None),))
0711         physics = _task_physics({(int(r[0]), r[1]) for r in rows})
0712         from swf_epicprod.quiet_end import quiet_task_ids
0713         quiet = quiet_task_ids()        # ended quietly: never on the floor (EPICPROD_OPS.md)
0714         rows = [r for r in rows if int(r[0]) not in quiet]
0715         for tid, name, queue, state, at, n, done, lost in rows:
0716             if at is not None and at.tzinfo is None:
0717                 at = at.replace(tzinfo=dt_timezone.utc)  # PanDA stores UTC
0718             local = at.astimezone(ZoneInfo('America/New_York')) if at else None
0719             n, done, lost = int(n or 0), int(done or 0), int(lost or 0)
0720             where = site_name.get(queue, queue) or 'no queue'
0721             what = physics.get(int(tid), '')
0722             items.append({
0723                 'id': int(tid), 'name': name, 'queue': queue or '', 'status': state,
0724                 'at': at.isoformat() if at else None, 'physics': what,
0725                 'link': f'{PUBLIC}/panda/tasks/{int(tid)}/',
0726                 'detail': (f"{int(tid)} {state} on {where}"
0727                            + (f" ({queue})" if queue and queue != where else '')
0728                            + (f" at {local:%m-%d %H:%M} ET" if local else '')
0729                            + f"; {done:,} of {n:,} files done, {lost:,} lost."
0730                            + (f" {what}." if what else '') + f" {name}")})
0731         return {'tasks': len(items), 'window_hours': hours,
0732                 'link': f'{PUBLIC}/panda/tasks/?processingtype=epicproduction&days=1',
0733                 'detail': (f"{len(items)} task{'s' if len(items) != 1 else ''} completed in {hours} h"
0734                            if items else f'no production task completed in {hours} h'),
0735                 'items': items}
0736 
0737     def opened(self):
0738         """Harvester workers per queue in the last hour: started (a slot
0739         opened: the pilot got a slot at the site) and submitted (sent to
0740         the site's batch system or pool, not yet a slot)."""
0741         sql = f'''SELECT computingsite,
0742                          SUM(CASE WHEN starttime >= %s THEN 1 ELSE 0 END),
0743                          SUM(CASE WHEN submittime >= %s THEN 1 ELSE 0 END)
0744                   FROM "{self.S}"."harvester_workers"
0745                   WHERE starttime >= %s OR submittime >= %s GROUP BY 1'''
0746         h = self.hour_ago
0747         return {site: (int(s or 0), int(n or 0)) for site, s, n in _rows(sql, (h, h, h, h))}
0748 
0749     def workers_now(self):
0750         """Harvester workers per queue now, from PanDA's own table: (running,
0751         submitted). The pilot-supply reading of a queue whose harvester
0752         sends us no report (NERSC's, run by PanDA operations)."""
0753         sql = f'''SELECT computingsite,
0754                          SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END),
0755                          SUM(CASE WHEN status = 'submitted' THEN 1 ELSE 0 END)
0756                   FROM "{self.S}"."harvester_workers"
0757                   WHERE status IN ('running', 'submitted') AND submittime >= %s GROUP BY 1'''
0758         return {site: (int(r or 0), int(s or 0))
0759                 for site, r, s in _rows(sql, (self.now - timedelta(days=7),))}
0760 
0761     def queue_status(self, names):
0762         sql = f'''SELECT panda_queue, data->>'status' FROM "{self.S}"."schedconfig_json"
0763                   WHERE panda_queue = ANY(%s)'''
0764         return {q: (st or '') for q, st in _rows(sql, (list(names),))}
0765 
0766     def jlab_worker_outcomes(self):
0767         """Commissioning evidence, including failures before a pilot gets a job.
0768 
0769         Harvester can stamp starttime when it discovers a finished worker, so
0770         this deliberately reports statuses, not inferred starts or runtimes.
0771         """
0772         rows = _rows(f'''SELECT status, COUNT(*)
0773                         FROM "{self.S}"."harvester_workers"
0774                         WHERE computingsite = %s AND submittime >= %s
0775                         GROUP BY status''',
0776                      ('JLAB_PanDA_1', self.now - timedelta(hours=48)))
0777         counts = {status: int(n) for status, n in rows}
0778         latest = _rows(f'''SELECT workerid, status, diagmessage, submittime
0779                           FROM "{self.S}"."harvester_workers"
0780                           WHERE computingsite = %s ORDER BY submittime DESC LIMIT 1''',
0781                        ('JLAB_PanDA_1',))
0782         last = latest[0] if latest else None
0783         detail = ', '.join(f'{n} {status}' for status, n in sorted(counts.items()))
0784         reason = f'commissioning; workers submitted in 48 h: {detail or "none"}'
0785         if last:
0786             reason += f'; latest worker {last[0]}: {last[1]}'
0787             if last[2]:
0788                 reason += f' ({str(last[2])[:240]})'
0789         return {'counts_48h': counts, 'latest_worker': last[0] if last else None,
0790                 'latest_submitted_at': last[3].isoformat() if last and last[3] else None,
0791                 'reason': reason,
0792                 'level': 'red' if counts.get('failed') or counts.get('cancelled') else 'grey'}
0793 
0794     def front_decisions(self, names):
0795         from monitor_app.models import AppLog
0796         out = {}
0797         since = self.now - timedelta(hours=3)
0798         for q in names:
0799             rec = (AppLog.objects.filter(app_name='epicprod', timestamp__gte=since,
0800                                          extra_data__action='front_decision',
0801                                          extra_data__subject_key=q)
0802                    .order_by('-timestamp').values_list('extra_data', flat=True).first())
0803             if rec:
0804                 out[q] = rec
0805         return out
0806 
0807     def osg_pool(self):
0808         from monitor_app.host_reports import latest
0809         found = latest(OSG_SUBMIT_HOST)
0810         if not found:
0811             return None
0812         record = found.get('record') or {}
0813         pool = record.get('pool') or {}
0814         return {'admitted': pool.get('slots_admitted'), 'total': pool.get('slots_total'),
0815                 'age_seconds': found.get('age_seconds'),
0816                 'fair_share': _fair_share(record.get('negotiators'), found.get('age_seconds'))}
0817 
0818     def pilots(self):
0819         """Per queue, the pilot slots waiting at the site and running by
0820         harvester's own count, its waiting limit, and for the OSG queues
0821         the idle pilots the submit schedd holds, from the host reporters'
0822         records (no remote call)."""
0823         from monitor_app.host_reports import latest
0824         out = {}
0825         for key in HARVESTER_REPORTS:
0826             found = latest(key)
0827             if not found:
0828                 continue
0829             record = found.get('record') or {}
0830             db = record.get('harvester_db') or {}
0831             now = db.get('workers_now') or {}
0832             try:
0833                 demand_at = datetime.fromisoformat(db['cache_updates']['job_statistics.json']).replace(tzinfo=dt_timezone.utc)
0834                 demand_age = (self.now - demand_at).total_seconds()
0835             except (KeyError, ValueError, TypeError):
0836                 demand_age = None
0837             # the limit harvester applied, from its adjuster log; pq_table's
0838             # n_queue_limit_worker is only the cap it is computed under
0839             applied = (record.get('adjustment') or {}).get('queues') or {}
0840             limits = {r['queue']: r for r in (record.get('harvester_db') or {}).get('launch_limits', [])
0841                       if isinstance(r, dict) and 'queue' in r}
0842             for q in (set(now) - {'error'}) | set(applied) | set(limits):
0843                 w = now.get(q) or {}
0844                 a = applied.get(q) or {}
0845                 limit = effective_waiting_limit(limits.get(q), a, self.now)
0846                 fixed = q in limits and _fixed_limit(limits[q])
0847                 adjustment_error = ((record.get('adjustment') or {}).get('read') or {}).get('error')
0848                 out[q] = {
0849                     'error': db.get('error') or now.get('error') or (
0850                         f'cannot read adjuster log: {adjustment_error}' if adjustment_error and not fixed else None),
0851                     'demand_age_seconds': demand_age,
0852                     'demand_max_age_seconds': DEMAND_MAX_AGE_SECONDS.get(key, 900),
0853                     'report_stale_seconds': 180 if key == 'osgsub01-harvester' else REPORT_STALE_SECONDS,
0854                     # harvester's idle is a pilot evicted back to waiting
0855                     'waiting': int(w.get('submitted') or 0) + int(w.get('idle') or 0),
0856                     'waiting_schedd': None,
0857                     'limit': limit,
0858                     'limit_source': 'database' if fixed else 'adjuster',
0859                     'limit_at': a.get('at'),
0860                     'max_workers': (limits.get(q) or {}).get('max_workers'),
0861                     'running': int(w.get('running') or 0),
0862                     'per_slot': PILOTS_PER_WORKER.get(q, 1),
0863                     'report': key,
0864                     'report_age_seconds': found.get('age_seconds'),
0865                 }
0866         found = latest(OSG_SUBMIT_HOST)
0867         idle = ((found or {}).get('record') or {}).get('idle_analysis')
0868         if isinstance(idle, dict) and 'error' not in idle:
0869             # the reporter lists every queue that has an idle pilot
0870             for q, p in out.items():
0871                 if p['report'] == f'{OSG_SUBMIT_HOST}-harvester':
0872                     p['waiting_schedd'] = int((idle.get(q) or {}).get('idle') or 0)
0873                     p['schedd_age_seconds'] = found.get('age_seconds')
0874         return {q: _pilot_display(p) for q, p in out.items()}
0875 
0876     def execute_sites(self, queues):
0877         sql = f'''SELECT COALESCE(NULLIF(destinationsite, ''), ''), COUNT(*),
0878                          SUM(GREATEST(COALESCE(corecount, 1), 1))
0879                   FROM "{self.S}"."jobsactive4"
0880                   WHERE jobstatus = 'running' AND computingsite = ANY(%s) GROUP BY 1'''
0881         running = {d: (int(n), int(c or 0)) for d, n, c in _rows(sql, (list(queues),))}
0882         sql = f'''
0883             SELECT COALESCE(NULLIF(destinationsite, ''), ''), jobstatus, COUNT(*),
0884                    SUM(GREATEST(COALESCE(corecount, 1), 1)
0885                        * EXTRACT(EPOCH FROM (endtime - starttime))) / 3600.0
0886             FROM (
0887               SELECT destinationsite, jobstatus, corecount, endtime, starttime FROM "{self.S}"."jobsactive4"
0888                WHERE endtime >= %s AND computingsite = ANY(%s) AND jobstatus IN ('finished', 'failed')
0889               UNION ALL
0890               SELECT destinationsite, jobstatus, corecount, endtime, starttime FROM "{self.S}"."jobsarchived4"
0891                WHERE endtime >= %s AND computingsite = ANY(%s) AND jobstatus IN ('finished', 'failed')) t
0892             GROUP BY 1, 2'''
0893         ended = {}
0894         for d, status, n, core_h in _rows(sql, (self.hour_ago, list(queues), self.hour_ago, list(queues))):
0895             e = ended.setdefault(d, {'finished': 0, 'failed': 0,
0896                                      'finished_core_h': 0.0, 'failed_core_h': 0.0})
0897             e[status] = int(n)
0898             e[f'{status}_core_h'] = round(float(core_h or 0), 1)
0899         out = []
0900         for name in sorted(set(running) | set(ended),
0901                            key=lambda s: -(running.get(s, (0, 0))[0])):
0902             r_jobs, r_cores = running.get(name, (0, 0))
0903             e = ended.get(name, {'finished': 0, 'failed': 0,
0904                                  'finished_core_h': 0.0, 'failed_core_h': 0.0})
0905             label = _uncut_site(name) or 'not reported'
0906             out.append({
0907                 'id': re.sub(r'[^a-z0-9]+', '-', label.lower()).strip('-'),
0908                 'name': label,
0909                 'short': short_name(label),
0910                 'running': r_jobs,
0911                 'finished_1h': e['finished'], 'failed_1h': e['failed'],
0912                 'capacity': None, 'capacity_source': None,
0913                 'in_use': r_cores,
0914                 'finished_core_h_1h': e['finished_core_h'],
0915                 'failed_core_h_1h': e['failed_core_h'],
0916                 'health': _health({'prod_activity': _activity_part(e['finished'], e['failed'])}),
0917                 # the execute site's jobs, by the site its pilot reported
0918                 'links': ({'jobs': f'{PUBLIC}/panda/jobs/?execsite={quote(name)}&days=1'}
0919                           if name else {}),
0920             })
0921         return out
0922 
0923     def science_arrivals(self, since=None, until=None, bin_seconds=None):
0924         """Per queue and output kind, the science files the last hour's
0925         finished production jobs registered in JLab Rucio, with their bytes
0926         and events: each DID the payload report's registration lists,
0927         joined by file name to the output it reports. A clash registered
0928         under a derived name (outcome ``diverted``) is a registered file
0929         whose name matches no output, so it counts with kind ``other`` and
0930         no bytes. PanDA's own file table holds no science output, since
0931         production tasks register their outputs themselves. ``since`` and
0932         ``until`` set another window; ``bin_seconds`` keys the result by
0933         the end of the job's end-time bin (epoch seconds) in place of the
0934         queue."""
0935         key = (f'((FLOOR(EXTRACT(EPOCH FROM d.endtime) / {int(bin_seconds)}) + 1)'
0936                f' * {int(bin_seconds)})::bigint' if bin_seconds else 'd.site')
0937         sql = f'''
0938             WITH r AS (
0939               SELECT j.computingsite AS site, j.endtime, m.metadata::jsonb -> 'payload' AS p
0940               FROM "{self.S}"."metatable" m JOIN "{self.S}"."jobsarchived4" j ON j.pandaid = m.pandaid
0941               WHERE j.endtime >= %s AND j.endtime < %s
0942                 AND j.processingtype = 'epicproduction' AND j.jobstatus = 'finished'
0943                 AND (j.jobmetrics LIKE %s OR j.jobmetrics LIKE %s)),
0944             d AS (
0945               SELECT r.site, r.endtime, r.p, did FROM r, jsonb_array_elements_text(
0946                 CASE WHEN jsonb_typeof(r.p -> 'registration' -> 'dids') = 'array'
0947                      THEN r.p -> 'registration' -> 'dids' ELSE '[]'::jsonb END) did)
0948             SELECT {key}, COALESCE(o.key, 'other'), COUNT(*),
0949                    SUM((o.value ->> 'bytes')::bigint), SUM((o.value ->> 'events')::bigint)
0950             FROM d LEFT JOIN LATERAL (
0951               SELECT o.key, o.value FROM jsonb_each(
0952                 CASE WHEN jsonb_typeof(d.p -> 'outputs') = 'object'
0953                      THEN d.p -> 'outputs' ELSE '{{}}'::jsonb END) o
0954               WHERE d.did LIKE '%%/' || (o.value ->> 'file') LIMIT 1) o ON true
0955             GROUP BY 1, 2'''
0956         out = {}
0957         for k, kind, n, nbytes, events in _rows(sql, (since or self.hour_ago, until or self.now,
0958                                                       '%payloadRegistration=registered%',
0959                                                       '%payloadRegistration=diverted%')):
0960             out.setdefault(int(k) if bin_seconds else (k or ''), {})[kind] = (
0961                 int(n), int(nbytes or 0), int(events or 0))
0962         return out
0963 
0964     def registration_outcomes(self, since=None, until=None, bin_seconds=None):
0965         """Per queue, the last hour's finished production jobs by the
0966         payload's registration outcome (``payloadRegistration``), and the
0967         end time of the newest that registered. ``since``, ``until`` and
0968         ``bin_seconds`` as for ``science_arrivals``."""
0969         key = (f'((FLOOR(EXTRACT(EPOCH FROM endtime) / {int(bin_seconds)}) + 1)'
0970                f' * {int(bin_seconds)})::bigint' if bin_seconds else 'computingsite')
0971         sql = f'''
0972             SELECT {key}, substring(jobmetrics from 'payloadRegistration=([A-Za-z_]+)'),
0973                    COUNT(*), MAX(endtime)
0974             FROM "{self.S}"."jobsarchived4"
0975             WHERE endtime >= %s AND endtime < %s
0976               AND processingtype = 'epicproduction' AND jobstatus = 'finished'
0977             GROUP BY 1, 2'''
0978         out, last = {}, None
0979         for k, outcome, n, end in _rows(sql, (since or self.hour_ago, until or self.now)):
0980             out.setdefault(int(k) if bin_seconds else (k or ''), {})[outcome or ''] = int(n)
0981             if outcome == 'registered' and end is not None and (last is None or end > last):
0982                 last = end
0983         return out, _aware(last)
0984 
0985     def log_arrivals(self):
0986         """Per queue, the log tarballs of the last hour's ended production
0987         jobs that reached BNL Rucio, with their bytes, and the jobs that
0988         failed at log stage-out. A failed job's log is registered too
0989         (PanDA marks every file of a failed job failed, but its log is at
0990         BNL_PROD_DISK_1, checked 2026-10-22), so only a job failed at log
0991         stage-out, or one whose pilot held its log in the stage-out bucket
0992         (``logHeld``), has no log there."""
0993         stageout = (f"(j.piloterrorcode IN ({','.join(str(c) for c in LOG_STAGEOUT_CODES)}) "
0994                     f"OR j.piloterrordiag ILIKE %s)")
0995         missing = f"((j.jobstatus = 'failed' AND {stageout}) OR COALESCE(j.jobmetrics, '') LIKE %s)"
0996         sql = f'''
0997             SELECT j.computingsite,
0998                    SUM(CASE WHEN {missing} THEN 0 ELSE 1 END),
0999                    SUM(CASE WHEN {missing} THEN 0 ELSE f.fsize END),
1000                    SUM(CASE WHEN j.jobstatus = 'failed' AND {stageout} THEN 1 ELSE 0 END),
1001                    MAX(CASE WHEN {missing} THEN NULL ELSE j.endtime END)
1002             FROM (
1003               SELECT pandaid, computingsite, jobstatus, jobmetrics, piloterrorcode, piloterrordiag, endtime
1004                 FROM "{self.S}"."jobsarchived4"
1005                WHERE endtime >= %s AND processingtype = 'epicproduction' AND jobstatus IN ('finished', 'failed')
1006               UNION ALL
1007               SELECT pandaid, computingsite, jobstatus, jobmetrics, piloterrorcode, piloterrordiag, endtime
1008                 FROM "{self.S}"."jobsactive4"
1009                WHERE endtime >= %s AND processingtype = 'epicproduction' AND jobstatus IN ('finished', 'failed')) j
1010             JOIN "{self.S}"."filestable4" f ON f.pandaid = j.pandaid AND f.type = 'log'
1011             GROUP BY 1'''
1012         diag, held = '%stage-out%', '%logHeld=%'
1013         rows = _rows(sql, (diag, held, diag, held, diag, diag, held, self.hour_ago, self.hour_ago))
1014         out, last = {}, None
1015         for site, n, nbytes, failed, end in rows:
1016             out[site or ''] = (int(n or 0), int(nbytes or 0), int(failed or 0))
1017             if end is not None and (last is None or end > last):
1018                 last = end
1019         return out, _aware(last)
1020 
1021     def latest_finished(self, metrics_like, negate=False):
1022         """The end time of the newest finished production job in two days
1023         whose job metrics match (or, negated, do not match) the pattern:
1024         the newest arrival when none arrived in the last hour."""
1025         last = _rows(f'''
1026             SELECT MAX(endtime) FROM "{self.S}"."jobsarchived4"
1027             WHERE endtime >= %s AND processingtype = 'epicproduction' AND jobstatus = 'finished'
1028               AND COALESCE(jobmetrics, '') {'NOT ' if negate else ''}LIKE %s''',
1029                      (self.now - timedelta(days=2), metrics_like))[0][0]
1030         return _aware(last)
1031 
1032     def jlab_flow(self, edges):
1033         """The JLab output flow on the trends axis (COMP_SNAPSHOT.md,
1034         Storage): per bin, the science files finished production jobs
1035         registered and their bytes, and the jobs whose registration did not
1036         complete, grouped by the job's end, through the hour's own
1037         reducers. A bin's jobs are final once archived, so the bins are
1038         kept (cached product ``jlab_flow:v1``) and each build reads again
1039         the JLAB_FLOW_REREAD_BINS bins before the newest kept one, and
1040         backfills JLAB_FLOW_BACKFILL_BINS of the window's bins not yet
1041         kept: a read of the whole window at once takes minutes. A bin not
1042         yet read is null."""
1043         from .models import CachedProduct
1044 
1045         width = int((edges[1] - edges[0]).total_seconds())
1046         ends = [int(e.timestamp()) for e in edges[1:]]
1047         row = CachedProduct.objects.filter(key=JLAB_FLOW_KEY).first()
1048         kept = {int(k): v for k, v in ((row.value or {}).get('bins') or {}).items()} if row else {}
1049         first = int(edges[0].timestamp())
1050         # Spans (start, end] of bin ends to read: the newest bins again, and
1051         # one hour of the window's bins not yet kept, before the oldest kept.
1052         if kept:
1053             spans = [(max(first, max(kept) - JLAB_FLOW_REREAD_BINS * width), ends[-1])]
1054             oldest = min(t for t in kept if t >= ends[0]) if any(t >= ends[0] for t in kept) else ends[-1]
1055             if oldest - width > first:
1056                 spans.append((max(first, oldest - width - JLAB_FLOW_BACKFILL_BINS * width),
1057                               oldest - width))
1058         else:
1059             spans = [(max(first, ends[-1] - JLAB_FLOW_BACKFILL_BINS * width), ends[-1])]
1060         for start, end in spans:
1061             since = datetime.fromtimestamp(start, dt_timezone.utc)
1062             until = datetime.fromtimestamp(end, dt_timezone.utc)
1063             arrivals = self.science_arrivals(since=since, until=until, bin_seconds=width)
1064             outcomes, _last = self.registration_outcomes(since=since, until=until,
1065                                                          bin_seconds=width)
1066             for t in ends:
1067                 if start < t <= end:
1068                     kinds = arrivals.get(t, {}).values()
1069                     kept[t] = [sum(n for n, _b, _e in kinds), sum(b for _n, b, _e in kinds),
1070                                sum(outcomes.get(t, {}).get(k, 0) for k in REGISTRATION_OWED)]
1071         kept = {t: v for t, v in kept.items() if t >= ends[0]}
1072         from .cached_product import stores
1073         if stores():
1074             CachedProduct.objects.update_or_create(
1075                 key=JLAB_FLOW_KEY, defaults={'value': {'bins': {str(t): v for t, v in kept.items()}},
1076                                              'built_at': timezone.now()})
1077         column = {t: v for t, v in kept.items()}
1078         return {
1079             'files': [column[t][0] if t in column else None for t in ends],
1080             'bytes': [column[t][1] if t in column else None for t in ends],
1081             'registration_incomplete_jobs': [column[t][2] if t in column else None for t in ends],
1082         }
1083 
1084     def storage(self, site_of, edges=None):
1085         """The E1s' Rucio storage (docs/COMP_SNAPSHOT.md, Storage): what
1086         arrived in the last hour, what did not, what is still owed, the
1087         output RSE's capacity, and the health."""
1088         def site_id(queue):
1089             return site_of.get(queue) or re.sub(r'[^a-z0-9]+', '-', (queue or 'not reported').lower()).strip('-')
1090 
1091         # JLab Rucio: science data
1092         arrivals = self.part('science_arrivals', self.science_arrivals, None)
1093         outcomes, last = self.part('registration_outcomes', self.registration_outcomes, (None, None))
1094         reg = {}
1095         for per in (outcomes or {}).values():
1096             for k, n in per.items():
1097                 reg[k] = reg.get(k, 0) + n
1098         if outcomes is None:
1099             reg_part = _part('grey', None, 'registration outcomes not read')
1100         else:
1101             registered = reg.get('registered', 0) + reg.get('diverted', 0)
1102             unsettled = sum(reg.get(k, 0) for k in REGISTRATION_OWED)
1103             done = registered + unsettled
1104             if done == 0:
1105                 reg_part = _part('grey', 0, 'no finished jobs in the last hour')
1106             else:
1107                 share = unsettled / done
1108                 reg_part = _part('red' if share > 0.5 else 'amber' if share > 0.05 else 'green',
1109                                  round(share, 3),
1110                                  f'{registered} registered, {unsettled} not registered (pending or unfinished)')
1111         if last is None:
1112             last = self.part('latest_registration',
1113                              lambda: self.latest_finished('%payloadRegistration=registered%'), None)
1114         if last is None:
1115             arrival = _part('red', None, 'no output registered in two days')
1116             jlab_age = None
1117         else:
1118             jlab_age = max(0, int((self.now - last).total_seconds()))
1119             age_min = round(jlab_age / 60, 1)
1120             arrival = _part('green' if age_min <= 30 else 'amber' if age_min <= 120 else 'red',
1121                             age_min, f'latest output registered {age_min} min ago')
1122         jlab_sites, by_kind = {}, {}
1123         for queue, kinds in (arrivals or {}).items():
1124             s = jlab_sites.setdefault(site_id(queue), {'arrived_1h': 0, 'bytes_1h': 0, 'failed_1h': 0})
1125             for kind, (n, nbytes, events) in kinds.items():
1126                 s['arrived_1h'] += n
1127                 s['bytes_1h'] += nbytes
1128                 k = by_kind.setdefault(kind, {'files': 0, 'bytes': 0, 'events': 0})
1129                 k['files'] += n
1130                 k['bytes'] += nbytes
1131                 k['events'] += events
1132         for queue, per in (outcomes or {}).items():
1133             owed = sum(per.get(k, 0) for k in REGISTRATION_OWED)
1134             if owed:
1135                 jlab_sites.setdefault(site_id(queue), {'arrived_1h': 0, 'bytes_1h': 0,
1136                                                        'failed_1h': 0})['failed_1h'] += owed
1137         jlab_pending = self.part('jlab_pending', _jlab_pending, None)
1138         capacity = self.part('storage_record', _storage_capacity, None)
1139         flow = self.part('jlab_flow', lambda: self.jlab_flow(edges), None) if edges else None
1140 
1141         # BNL Rucio: logs
1142         logs, last_log = self.part('log_arrivals', self.log_arrivals, (None, None))
1143         if logs is None:
1144             log_part = _part('grey', None, 'log stage-out not read')
1145             log_failed = None
1146         else:
1147             log_failed = sum(f for _n, _b, f in logs.values())
1148             log_part = _part('red' if log_failed >= 50 else 'amber' if log_failed >= 5 else 'green',
1149                              log_failed, f'{log_failed} jobs failed at log stage-out in the last hour')
1150         bnl_sites = {}
1151         for queue, (n, nbytes, failed) in (logs or {}).items():
1152             s = bnl_sites.setdefault(site_id(queue), {'arrived_1h': 0, 'bytes_1h': 0, 'failed_1h': 0})
1153             s['arrived_1h'] += n
1154             s['bytes_1h'] += nbytes
1155             s['failed_1h'] += failed
1156         if last_log is None:
1157             last_log = self.part('latest_log',
1158                                  lambda: self.latest_finished('%logHeld=%', negate=True), None)
1159         log_pending = self.part('log_pending', _log_pending, None)
1160 
1161         def site_rows(sites):
1162             return [dict(site=k, **v) for k, v in
1163                     sorted(sites.items(), key=lambda kv: -kv[1]['arrived_1h'])]
1164 
1165         return [
1166             {'id': 'jlab-rucio', 'name': 'JLab Rucio', 'holds': 'science data',
1167              'status': 'live', 'counts': 'files',
1168              'arrived_1h': (sum(k['files'] for k in by_kind.values()) if arrivals is not None else None),
1169              'bytes_1h': (sum(k['bytes'] for k in by_kind.values()) if arrivals is not None else None),
1170              'events_1h': (sum(k['events'] for k in by_kind.values()) if arrivals is not None else None),
1171              'arrived_by_kind_1h': ({k: v['files'] for k, v in by_kind.items()}
1172                                     if arrivals is not None else None),
1173              'failed_1h': (sum(reg.get(k, 0) for k in REGISTRATION_OWED) if outcomes is not None else None),
1174              'pending': (jlab_pending or {}).get('pending'),
1175              'pending_parts': (jlab_pending or {}).get('parts'),
1176              'pending_at': (jlab_pending or {}).get('at'),
1177              'latest_arrival_s': jlab_age,
1178              'flow': flow,
1179              'by_site': site_rows(jlab_sites),
1180              'capacity': (capacity or {}).get('output'),
1181              'rses': (capacity or {}).get('rses') or [],
1182              'health': _health({'registration': reg_part, 'latest_arrival': arrival}),
1183              'links': {'storage': f'{PUBLIC}/panda/storage-doors/',
1184                        'arrived': f'{PUBLIC}/snapper/epicprod/storage/',
1185                        'failed': incomplete_registration_link(self.now),
1186                        'pending': f'{PUBLIC}/pcs/stash/'}},
1187             {'id': 'bnl-rucio', 'name': 'BNL Rucio', 'holds': 'logs',
1188              'status': 'live', 'counts': 'logs',
1189              'arrived_1h': (sum(n for n, _b, _f in logs.values()) if logs is not None else None),
1190              'bytes_1h': (sum(b for _n, b, _f in logs.values()) if logs is not None else None),
1191              'events_1h': None,
1192              'arrived_by_kind_1h': None,
1193              'failed_1h': log_failed,
1194              'pending': (log_pending or {}).get('pending'),
1195              'pending_parts': (log_pending or {}).get('parts'),
1196              'pending_at': (log_pending or {}).get('at'),
1197              'latest_arrival_s': (max(0, int((self.now - last_log).total_seconds()))
1198                                   if last_log is not None else None),
1199              'by_site': site_rows(bnl_sites),
1200              'capacity': None,
1201              'rses': [],
1202              'health': _health({'log_stageout': log_part}),
1203              'links': {'storage': f'{PUBLIC}/panda/storage-doors/',
1204                        'arrived': f'{PUBLIC}/panda/jobs/?status=finished&days=1',
1205                        'failed': f'{PUBLIC}/panda/errors/?error_source=pilot&days=1',
1206                        'pending': None}},
1207         ]
1208 
1209 
1210 def _primary_queue(queues):
1211     return max(queues, key=lambda q: (
1212         {'live': 3, 'commissioning': 2, 'off': 1}.get(q['status'], 0),
1213         q.get('running') or 0), default=None)
1214 
1215 
1216 def _production_display(queue, status, cfg):
1217     return status == 'live' or (
1218         queue == 'BNL_ePIC_GOOGLE_es' and status == 'commissioning'
1219         and bool(cfg.get('gke_pilots.enabled'))
1220         and int(cfg.get('gke_pilots.max_pods') or 0) > 0)
1221 
1222 
1223 def _sites_config():
1224     """The site groups of ``comp.sites``, each with its queues from the
1225     queue registry in the registry's order, shown with the registry's
1226     status; a registry site with no group is shown under its own id."""
1227     from monitor_app.models import SysConfig
1228     from swf_epicprod import queues as registry
1229     value = SysConfig.get_setting('comp.sites', DEFAULT_SITES)
1230     groups = [g for g in (value if isinstance(value, list) else DEFAULT_SITES)
1231               if isinstance(g, dict) and g.get('id')]
1232     by_site = registry.room_queues()
1233     out = []
1234     for g in [*groups, *({'id': s, 'name': s} for s in by_site if s not in {g['id'] for g in groups})]:
1235         site = {k: v for k, v in g.items() if k != 'queues'}
1236         site['name'] = short_name(site.get('name') or site['id'])
1237         site['queues'] = [{'id': e['queue'], 'status': registry.ROOM_STATUS.get(e['status']),
1238                            'status_reason': e['reason']} for e in by_site.get(g['id'], [])]
1239         out.append(site)
1240     return out
1241 
1242 
1243 def build():
1244     """Build the record (docs/COMP_SNAPSHOT.md, schema 1)."""
1245     from monitor_app.models import SysConfig
1246     from monitor_app.panda.census import queue_census
1247     from swf_epicprod import queues as registry
1248 
1249     t0 = time.monotonic()
1250     b = _Build()
1251     sites_cfg = _sites_config()
1252     names = [q['id'] for s in sites_cfg for q in s.get('queues', [])]
1253     cfg = SysConfig.get_config()
1254 
1255     census = b.part('census', lambda: (queue_census() or {}).get('queues') or {}, {})
1256     ended = b.part('ended', lambda: b.ended('computingsite'), {})
1257     failures = b.part('failures', b.top_failures, {})
1258     opened = b.part('harvester', b.opened, {})
1259     workers = b.part('harvester_workers', b.workers_now, {})
1260     pandastatus =b.part('queue_status', lambda: b.queue_status(names), {})
1261     decisions = b.part('front', lambda: b.front_decisions(names), {})
1262     pool = b.part('osg_pool', b.osg_pool, None)
1263     pilots = b.part('pilots', b.pilots, {})
1264     drift = b.part('queue_drift', _queue_drift, [])
1265     jlab_workers = (b.part('jlab_workers', b.jlab_worker_outcomes, None)
1266                     if 'JLAB_PanDA_1' in names else None)
1267 
1268     sites = []
1269     for s in sites_cfg:
1270         queues = []
1271         for qc in s.get('queues', []):
1272             q = qc['id']
1273             c = census.get(q) or {}
1274             by_status = c.get('by_status') or {}
1275             e = ended.get(q) or {'finished': 0, 'failed': 0, 'finished_core_h': 0.0, 'failed_core_h': 0.0}
1276             d = decisions.get(q) or {}
1277             dc = d.get('counts') or d
1278             running = int((by_status.get('running') or {}).get('jobs', 0))
1279             running_cores = int((by_status.get('running') or {}).get('cores', running))
1280             starting = int((by_status.get('starting') or {}).get('jobs', 0))
1281             in_panda = q in pandastatus
1282             status = qc.get('status') or ('live' if in_panda else 'not_built')
1283             reason = qc.get('status_reason') or ('' if in_panda else 'not in PanDA')
1284             if q == 'JLAB_PanDA_1' and status != 'live':
1285                 reason = (jlab_workers or {}).get('reason',
1286                            'commissioning; worker status unavailable')
1287 
1288             # capacity, best source first
1289             capacity, source = None, None
1290             configured = cfg.get(f'comp.capacity.{q}')
1291             if configured:
1292                 capacity, source = int(configured), 'configured'
1293             elif s.get('execute_sites') and pool and pool.get('admitted') is not None:
1294                 # one pool behind every OSG queue: the site carries it once
1295                 capacity, source = int(pool['admitted']), 'pool, shared'
1296             elif (c.get('calibration') or {}).get('peak_running'):
1297                 capacity, source = int(c['calibration']['peak_running']), 'observed peak'
1298 
1299             if status != 'live':
1300                 capacity, source = None, None
1301             if status == 'live':
1302                 activity = _activity_part(e['finished'], e['failed'])
1303                 canary = dc.get('canary') or 'unknown'
1304                 canary_part = _part({'healthy': 'green', 'failing': 'red'}.get(canary, 'grey'), canary,
1305                                     f'canary {canary}')
1306                 top = (failures.get(q) or [])
1307                 ended_n = e['finished'] + e['failed']
1308                 if not ended_n:
1309                     snap = _part('grey', 0, 'no jobs ended in the last hour')
1310                 elif top and top[0][1] * 2 > ended_n and e['failed'] >= 10:
1311                     snap = _part('red', top[0][1], f'{top[0][1]} x {top[0][0]}')
1312                 elif e['failed'] * 4 > ended_n:
1313                     snap = _part('amber', e['failed'], f'top: {top[0][1]} x {top[0][0]}' if top else '')
1314                 else:
1315                     snap = _part('green', e['failed'],
1316                                  f'top: {top[0][1]} x {top[0][0]}' if top else 'no failures')
1317                 if q in registry.AGENT_SUPPLIED:
1318                     supply = _gke_workers(cfg)[2]
1319                 else:
1320                     supply = _pilot_supply_part(pilots.get(q), int(c.get('not_started') or 0),
1321                                                 *opened.get(q, (0, 0)), workers=workers.get(q),
1322                                                 operator_limit=(_worker_limit(cfg, q) or {}).get('limit'))
1323                 health = _health({'prod_activity': activity, 'canary': canary_part,
1324                                   'snapper_errors': snap, 'pilot_supply': supply})
1325             else:
1326                 health = _health({'status': _part('grey', status, reason or status)})
1327                 if q == 'JLAB_PanDA_1':
1328                     health = _health({'status': _part(
1329                         (jlab_workers or {}).get('level', 'amber'), status, reason)})
1330 
1331             queues.append({
1332                 'id': q, 'name': qc.get('name') or q,
1333                 'status': status, 'status_reason': reason,
1334                 # PanDA's worker table now (running, submitted): the nodes a
1335                 # whole-node queue holds, held against its ceiling (Report).
1336                 'workers_now': (dict(zip(('running', 'submitted'), workers[q]))
1337                                 if workers.get(q) else None),
1338                 'production_display': _production_display(q, status, cfg),
1339                 'running': running, 'starting': starting,
1340                 'queued': int(c.get('not_started') or 0),
1341                 'ungenerated': int(c.get('ungenerated') or 0),
1342                 'harvester_slots_opened_1h': opened.get(q, (0, 0))[0],
1343                 'harvester_pilots_submitted_1h': opened.get(q, (0, 0))[1],
1344                 'pilots': pilots.get(q),
1345                 'finished_1h': e['finished'], 'failed_1h': e['failed'],
1346                 'capacity': capacity, 'capacity_source': source,
1347                 'in_use': running_cores,
1348                 'finished_core_h_1h': e['finished_core_h'],
1349                 'failed_core_h_1h': e['failed_core_h'],
1350                 'front': {'state': d.get('outcome'), 'reason': d.get('reason'),
1351                           'feed': dc.get('feed_switch')},
1352                 'health': health,
1353                 'links': {
1354                     'panda_queue': f'{PUBLIC}/panda/epic-queues/{q}/',
1355                     'snapper': f'{PUBLIC}/panda/errors/?site={q}',
1356                     # the canary root redirects, which the proxy cannot relay
1357                     'canary': f'{PUBLIC}/canary/probes/',
1358                     # the pillars' drilldowns: the queue's running jobs, its
1359                     # queued jobs (the census's not-started set, `queued`
1360                     # here), and its harvester workers
1361                     'running': f'{PUBLIC}/panda/jobs/?site={q}&status=running&days=2',
1362                     'queued': f'{PUBLIC}/panda/jobs/?site={q}&status=queued&days=30',
1363                     'slots': f'{PUBLIC}/panda/epic-queues/{q}/#workers',
1364                 },
1365             })
1366         if s.get('execute_sites') and pool and pool.get('admitted') is not None:
1367             site_capacity, site_source = int(pool['admitted']), 'pool'
1368         else:
1369             caps = [q['capacity'] for q in queues if q['capacity'] is not None]
1370             site_capacity = sum(caps) if caps else None
1371             site_source = ('+'.join(sorted({q['capacity_source'] for q in queues if q['capacity_source']}))
1372                            or None)
1373         site = {
1374             'id': s['id'], 'name': s['name'],
1375             'capacity': site_capacity, 'capacity_source': site_source,
1376             'in_use': sum(q['in_use'] for q in queues),
1377             'status': (_primary_queue(queues) or {}).get('status', 'off'),
1378             'health': {'level': _worst(q['health']['level'] for q in queues),
1379                        'parts': {q['id']: q['health']['level'] for q in queues}},
1380             # a site's links are those of its busiest live queue: for the
1381             # OSG pool, the queue production runs on
1382             'links': (dict(_primary_queue(queues)['links'])
1383                       if queues else {'panda_queue': None}),
1384             'queues': queues,
1385             'execute_sites': [],
1386         }
1387         if s.get('execute_sites'):
1388             site['fair_share'] = (pool or {}).get('fair_share')
1389             live = [q['id'] for q in queues if q['status'] == 'live']
1390             if live:
1391                 site['execute_sites'] = b.part('execute_sites', lambda: b.execute_sites(live), [])
1392         sites.append(site)
1393 
1394     site_of = {q['id']: s['id'] for s in sites_cfg for q in s.get('queues', [])}
1395     trend = b.part('trend_series', lambda: _trend_series(sites), None)
1396     storage = b.part('storage', lambda: b.storage(
1397         site_of, edges=trend['edges'] if trend else None), [])
1398 
1399     ready = b.part('ready_tasks', lambda: _ready_tasks(), None)
1400     released = b.part('released', lambda: _released(b.hour_ago), None)
1401     front = {
1402         'ready_tasks': ready, 'released_1h': released,
1403         'enabled': cfg.get('front.enabled'), 'mode': cfg.get('front.mode'),
1404         'jedi_throttled': cfg.get('front.jedi_throttled'),
1405         'link': f'{PUBLIC}/panda/front/#ready',
1406         **(b.part('front_board', lambda: _front_board(ready, released), None) or {}),
1407     }
1408 
1409     alarms = b.part('alarms', b.production_alarms, [])
1410     site_name = {q['id']: short_name(s.get('name') or s['id'])
1411                  for s in sites_cfg for q in s.get('queues', [])}
1412     record = {
1413         'schema': SCHEMA,
1414         'observed_at': b.now.isoformat(),
1415         'units': {'capacity': 'cores', 'core_hours': 'core-hours in the last hour'},
1416         'pressure_front': front,
1417         'sites': sites,
1418         'storage': storage,
1419         'report': {'text': _report_text(front, sites, storage),
1420                    'items': _drift_items(drift) + _report_items(
1421                        front, sites, storage, alarms,
1422                        b.part('assessment', _assessment_freshness, None)),
1423                    'assessment': b.part('daily_assessment', _latest_daily_assessment, None)},
1424         'live': b.part('live', _live_records, None),
1425         'tasks': b.part('tasks', lambda: b.floor_tasks(site_name, alarms, site_of), None),
1426         'paused': b.part('paused', lambda: b.paused_tasks(site_name), None),
1427         'completed': b.part('completed', lambda: b.completed_tasks(site_name), None),
1428         'arrivals': b.part('arrivals', lambda: b.pc_arrivals(site_name), None),
1429         'trends': b.part('trends', lambda: _trends(trend), None),
1430         'errors': b.errors,
1431     }
1432     record['build_seconds'] = round(time.monotonic() - t0, 2)
1433     return record
1434 
1435 
1436 def _ready_tasks():
1437     with connections['default'].cursor() as cursor:
1438         cursor.execute("SELECT COUNT(*) FROM pcs_prod_task WHERE overrides ? 'queued'")
1439         return int(cursor.fetchone()[0])
1440 
1441 
1442 def _front_board(ready, released):
1443     """What the pressure-front pillar shows on hover and opens on click
1444     (Torre Wenaus, 2026-10-22): the ready buffer task by task, in each
1445     queue's feed order, and each production queue's latest decision, from
1446     the state the front's cycle stored (cached product ``front_state``)."""
1447     from monitor_app.models import CachedProduct
1448     from swf_epicprod.front import STATE_KEY, closed_queues
1449 
1450     row = CachedProduct.objects.filter(key=STATE_KEY).first()
1451     state = (row.value if row else None) or {}
1452     closed = closed_queues()
1453     items = []
1454     for queue, entries in (state.get('backlog') or {}).items():
1455         for order, e in enumerate(entries or [], 1):
1456             items.append({
1457                 'task': e.get('task'), 'queue': None if queue == 'unplaced' else queue,
1458                 'order': order, 'kind': e.get('kind'), 'by_hand': bool(e.get('hand')),
1459                 'priority': e.get('level'), 'jobs': e.get('rows'),
1460                 'eligible': not e.get('problems') and queue not in closed,
1461                 'problems': list(e.get('problems') or [])
1462                 + ([f'queue closed: {closed[queue]}'] if queue in closed else []),
1463                 'link': f"{PUBLIC}/pcs/tasks/{e.get('task')}/"})
1464     queues = []
1465     for name, d in (state.get('queues') or {}).items():
1466         queues.append({
1467             'queue': name, 'state': d.get('state'), 'reason': d.get('reason'),
1468             'committed_h': d.get('committed_h'), 'h_low': d.get('h_low'),
1469             'h_high': d.get('h_high'), 'running': d.get('running'),
1470             'ready_total': d.get('ready_total'), 'ready_eligible': d.get('ready_eligible'),
1471             'last_feed_age_h': d.get('last_feed_age_h'), 'closed': name in closed})
1472     lines = [f"{ready if ready is not None else '?'} tasks ready, "
1473              f"{released if released is not None else '?'} released in the last hour"]
1474     for q in queues:
1475         waiting = [i for i in items if i['queue'] == q['queue']]
1476         if not waiting and q['closed']:
1477             continue
1478         lines.append(
1479             f"{short_name(q['queue'])}: {q['state'] or '?'}"
1480             + (f", {q['committed_h']:.1f} h committed of {q['h_low']:g}-{q['h_high']:g} h"
1481                if isinstance(q['committed_h'], (int, float)) and q['h_low'] is not None else '')
1482             + (f"; {len(waiting)} ready, next {waiting[0]['task']}" if waiting else '')
1483             + (' (closed)' if q['closed'] else ''))
1484     decided = {q['queue'] for q in queues}
1485     for queue in dict.fromkeys(i['queue'] for i in items if i['queue'] not in decided):
1486         waiting = [i for i in items if i['queue'] == queue]
1487         lines.append(f"{short_name(queue) or 'no queue yet'}: {len(waiting)} ready, next {waiting[0]['task']}"
1488                      + (' (queue closed, held)' if queue in closed else ''))
1489     return {'items': items, 'queues': queues, 'cycle_at': state.get('cycle_at'),
1490             'detail': '. '.join(lines) + '.'}
1491 
1492 
1493 def _released(since):
1494     from monitor_app.models import AppLog
1495     return AppLog.objects.filter(app_name='epicprod', timestamp__gte=since,
1496                                  extra_data__action='front_decision',
1497                                  extra_data__outcome='fed').count()
1498 
1499 
1500 def _fair_share_notice(share, link):
1501     """The notice shared by every display of the pool's fair-share state."""
1502     if not share or not share.get('limited'):
1503         return None
1504     pools = ' and '.join(share.get('pools') or ['Pool'])
1505     cycle = share.get('cycle_at')
1506     when = ''
1507     if cycle:
1508         try:
1509             at = datetime.fromisoformat(cycle.replace('Z', '+00:00'))
1510             when = f" ({at.astimezone(ZoneInfo('America/New_York')):%Y-%m-%d %H:%M:%S} ET)"
1511         except (TypeError, ValueError):
1512             logger.warning('Unusable fair-share cycle time: %r', cycle)
1513     # Informational: the pool holding us at our share is OSG's normal mode
1514     # (Torre Wenaus, 2026-10-28), not a warning, and never colours OSG.
1515     return {
1516         'text': 'fair share limited', 'level': 'grey', 'link': link,
1517         'detail': (f'{pools}: fair-share limit reached in the last reported '
1518                    f'negotiation cycle{when}. Existing pilots keep their slots; '
1519                    'later cycles may grant more.'),
1520     }
1521 
1522 
1523 def _fair_share(negotiators, age_seconds):
1524     """Whether a pool's negotiator held our OSG submitter at its
1525     fair-share limit in its last cycle, from the submit host reporter's
1526     record (docs/OSG_SUBMIT_REPORTER.md).
1527     None when the record lacks the reading or is stale."""
1528     if (not isinstance(negotiators, dict) or not negotiators.get('negotiators')
1529             or age_seconds is None or age_seconds > REPORT_STALE_SECONDS):
1530         return None
1531     held = [n for n in negotiators['negotiators'] if n.get('share_limited')]
1532     return {'limited': bool(held),
1533             'pools': sorted({short_name(n['pool']) for n in held}),
1534             'cycle_at': max((n['cycle_at'] for n in held if n.get('cycle_at')), default=None),
1535             'unread': sorted({short_name(p) for p in negotiators.get('errors') or {}})}
1536 
1537 
1538 def _queue_drift():
1539     """Where a host's copy of a queue's status disagrees with the queue
1540     registry, from the host reporters' records (swf-epicprod
1541     docs/QUEUE_REGISTRY.md, The drift check)."""
1542     from monitor_app.host_reports import latest
1543     from swf_epicprod import queues as registry
1544     return registry.drift({key: latest(key) for key in registry.drift_reports()})
1545 
1546 
1547 def _drift_items(drift):
1548     """One red report line per disagreement: the copy must be put right."""
1549     return [{'level': 'red', 'key': f"drift:{d['queue']}:{d['copy']}",
1550              'text': (f"Queue drift, {d['queue']}: {d['copy']} holds {d['holds']}, "
1551                       f"the queue registry says {d['expected']}"),
1552              'link': f'{PUBLIC}/panda/front/'} for d in drift or []]
1553 
1554 
1555 def _report_text(front, sites, storage):
1556     lines = []
1557     ready, released = front.get('ready_tasks'), front.get('released_1h')
1558     lines.append(f"Pressure front: {ready if ready is not None else '?'} tasks ready, "
1559                  f"{released if released is not None else '?'} released in the last hour.")
1560     for s in sites:
1561         live = [q for q in s['queues'] if q['status'] == 'live']
1562         if not live:
1563             continue
1564         running = sum(q['running'] for q in live)
1565         fin = sum(q['finished_1h'] for q in live)
1566         fail = sum(q['failed_1h'] for q in live)
1567         queued = sum(q['queued'] for q in live)
1568         lines.append(f"{s['name']}: {running} running, {queued} queued; last hour "
1569                      f"{fin} finished, {fail} failed ({s['health']['level']}).")
1570         for q in live:
1571             for name, p in q['health']['parts'].items():
1572                 if p['level'] in ('amber', 'red'):
1573                     lines.append(f"  {q['id']} {name} {p['level']}: {p['note']}")
1574     off = [s['name'] for s in sites if not any(q['status'] == 'live' for q in s['queues'])]
1575     if off:
1576         lines.append(f"Off or not yet built: {', '.join(off)}.")
1577     for st in storage:
1578         for name, p in st['health']['parts'].items():
1579             if p['level'] in ('amber', 'red'):
1580                 lines.append(f"{st['name']} {name} {p['level']}: {p['note']}")
1581     return '\n'.join(lines)
1582 
1583 
1584 # What a front decision means, in the words CONTINUOUS_PRODUCTION.md gives
1585 # its states and reason codes, with the level the report shows it at.
1586 FRONT_STATES = {
1587     'supplied': ('green', 'enough work queued, nothing to feed'),
1588     'fed': ('green', 'fed a task in its last cycle'),
1589     'awaiting_observation': ('grey', 'waiting for its last submission to activate'),
1590     'degraded': ('red', 'holds new releases, a gate is red'),
1591     'half_open': ('amber', 'trying one task after a gate cleared'),
1592     'held': ('grey', 'held'),
1593     'oversize': ('amber', 'the next task alone is too large, waiting for the operator'),
1594     'no_work': ('amber', 'no eligible task in the ready buffer'),
1595 }
1596 FRONT_REASONS = {
1597     'queue_off': 'its feed is off', 'front_off': 'the front is off',
1598     'queue_exempt': 'its own feeder supplies it',
1599     'declared': 'a declared downtime', 'would_exceed_high': 'a feed would pass the high mark',
1600     'task_cap': 'the task cap', 'job_cap': 'the job cap',
1601     'unsized_task': 'a task that cannot be sized', 'no_calibration': 'no calibration yet',
1602     'canary': 'the canary is failing', 'burn_through': 'jobs burning through',
1603     'failure_window': 'the failure rate over its threshold', 'credential': 'a credential',
1604 }
1605 PART_NAMES = {'prod_activity': 'jobs', 'canary': 'canary',
1606               'snapper_errors': 'failures', 'pilot_supply': 'pilot supply'}
1607 LEVEL_ORDER = {'red': 0, 'amber': 1, 'grey': 2, 'green': 3}
1608 # The alarm engine's titles: 'task 40962 (failed) final-failure rate 100.0% —
1609 # <task name>'.
1610 TASK_ALARM_RE = re.compile(r'^task (\d+) \(([^)]*)\) final-failure rate ([\d.]+%) — (.*)$')
1611 # A ping event's title as the engine first recorded it (swf_alarms pings.py).
1612 DUE_RE = re.compile(r'^(?:due in \d+ days?|due today|overdue by \d+ days?) '
1613                     r'\((\d{4}-\d\d-\d\d)\): (.*)$')
1614 
1615 
1616 def _active_alarms():
1617     """The alarm engine's events not cleared (docs/alarms.md)."""
1618     from monitor_app.alarms_data import CONTEXT_NAME
1619     from monitor_app.models import Entry
1620     rows = (Entry.objects.filter(context_id=CONTEXT_NAME, kind='event',
1621                                  archived=False, deleted_at__isnull=True)
1622             .order_by('-timestamp_created')[:500])
1623     return [{'id': str(e.id), 'title': e.title or (e.data or {}).get('subject') or '',
1624              'severity': (e.data or {}).get('severity') or 'alarm'}
1625             for e in rows if (e.data or {}).get('clear_time') is None]
1626 
1627 
1628 def _assessment_freshness():
1629     """Whether the scheduled campaign assessments arrived (the System
1630     page's campaign-assessments row). The verdicts themselves are held
1631     by corun-ai, which this DB-only build does not call."""
1632     from swf_epicprod.assessment.freshness import assessment_freshness
1633     status, summary, _data = assessment_freshness()
1634     return {'status': status, 'summary': summary}
1635 
1636 
1637 def _latest_daily_assessment():
1638     """The newest daily campaign assessment (swf-epicprod
1639     EPICPROD_ASSESSMENTS.md), from its ``assessment_register`` record: the
1640     verdict, the verdict as a health level, and the assessment's page."""
1641     from monitor_app.models import AppLog
1642     from swf_epicprod.assessment.freshness import DAILY_KINDS
1643     rec = (AppLog.objects.filter(app_name='epicprod', extra_data__action='assessment_register',
1644                                  extra_data__subject_type='campaign',
1645                                  extra_data__assessment_kind__in=DAILY_KINDS)
1646            .order_by('-timestamp').values('timestamp', 'extra_data').first())
1647     if not rec:
1648         return None
1649     x = rec['extra_data'] or {}
1650     path = x.get('url') or x.get('report_path')
1651     verdict = x.get('verdict') or ''
1652     return {'verdict': verdict, 'level': VERDICT_LEVELS.get(verdict, 'grey'),
1653             'at': rec['timestamp'].isoformat(),
1654             'link': f'{PUBLIC}{path}' if path else None,
1655             'campaign': x.get('subject_key'), 'title': x.get('report_title'),
1656             'narration': x.get('narration')}
1657 
1658 
1659 def _live_move_hours():
1660     """How long a task Move stays on the epicprod-live screen, SysConfig
1661     ``comp.live.move_hours``."""
1662     from monitor_app.models import SysConfig
1663     value = SysConfig.get_setting('comp.live.move_hours', LIVE_MOVE_HOURS)
1664     if isinstance(value, (int, float)) and not isinstance(value, bool) and value >= 0:
1665         return value
1666     logger.warning('comp.live.move_hours %r is not a number of hours; using %s',
1667                    value, LIVE_MOVE_HOURS)
1668     return LIVE_MOVE_HOURS
1669 
1670 
1671 def _move_detail(extra, at, task_name):
1672     """A Move's detail on the screen: the task, its route, who asked and
1673     why (live_curated.task_operation_who_why), and its state unless done."""
1674     from monitor_app.live_curated import task_operation_who_why
1675     who, why = task_operation_who_why(extra, at)
1676     text = (f"{short_name(extra.get('previous_site')) or '?'} to "
1677             f"{short_name(extra.get('site')) or '?'}, by {who}")
1678     if why:
1679         text += f': {why}'
1680     if extra.get('outcome') != 'ok':
1681         text += f"; {extra.get('reason') or extra.get('outcome')}"
1682     return f'{task_name}: {text}' if task_name else text
1683 
1684 
1685 def _feed_text(extra):
1686     """(title, detail) for a task the front released from the ready buffer
1687     to a queue: where from and where to (Torre Wenaus, 2026-10-29: "eg from
1688     ready buffer to perlmutter"), the task by its physics and evgen tags,
1689     and why the queue took it."""
1690     from monitor_app.live_notices import task_short
1691     queue = short_name(extra.get('subject_key')) or '?'
1692     task = str(extra.get('candidate') or '')
1693     short = task_short(task)
1694     title = f'Move ready buffer to {queue}' + (f' · {short}' if short else '')
1695     why = (f"{queue} below its low mark: {extra.get('committed_h')} h committed "
1696            f"of {extra.get('h_low')} to {extra.get('h_high')} h")
1697     return title, (f'{task}: ready buffer to {queue}, by front: {why}' if task
1698                    else f'ready buffer to {queue}, by front: {why}')
1699 
1700 
1701 def _live_records():
1702     """The epicprod-live screen: the newest warning and error records of
1703     the live stream, the logs page's live view (ACTION_STREAM.md), each
1704     with its own page, and a task Move at INFO for ``comp.live.move_hours``.
1705     The room reads the record anonymously and the logs page is behind the
1706     login wall, so the records ride here. The floor is live now: records
1707     from the window only, a node guard trip only for a node new to the
1708     blacklist, and off it a record a ``live_cleared`` record names, a
1709     failure whose action has since run ok, and a record a later one on its
1710     subject supersedes or repeats (COMP_SNAPSHOT.md, Live)."""
1711     from django.db.models import Q
1712     from monitor_app.epicprod_logging import EPICPROD_APP_NAME, live_stream_q
1713     from monitor_app.live_notices import (FAILURE_OUTCOMES, board_text, failure_key,
1714                                          site_node_guard_fields)
1715     from monitor_app.models import AppLog
1716     cleared = set()
1717     for ids in (AppLog.objects
1718                 .filter(app_name=EPICPROD_APP_NAME,
1719                         extra_data__action='live_cleared')
1720                 .values_list('extra_data__cleared_ids', flat=True)):
1721         cleared.update(int(i) for i in ids or ())
1722     moves_since = timezone.now() - timedelta(hours=_live_move_hours())
1723     rows = (AppLog.objects.filter(live_stream_q())
1724             .filter(Q(levelname__in=LIVE_LEVELS)
1725                     | Q(levelname='INFO', timestamp__gte=moves_since,
1726                         extra_data__action='panda_task_operation',
1727                         extra_data__operation='reassign')
1728                     | Q(levelname='INFO', timestamp__gte=moves_since,
1729                         extra_data__action='front_decision', extra_data__outcome='fed'))
1730             .exclude(id__in=cleared)
1731             .order_by('-timestamp')
1732             .values('id', 'timestamp', 'levelname', 'message', 'instance_name',
1733                     'extra_data'))
1734     cutoff = timezone.now() - timedelta(hours=LIVE_WINDOW_H)
1735     ranks = None
1736     last_ok = {}
1737     shown = set()
1738     items = []
1739     for r in _pages(rows):
1740         if r['timestamp'] < cutoff:
1741             break
1742         extra = r['extra_data'] or {}
1743         # A node guard trip is news only for a node not already on the
1744         # blacklist; a re-trip from half open is a node the floor knows.
1745         if (extra.get('action') == 'node_guard_decision'
1746                 and extra.get('old_state') not in (None, '', 'clear')):
1747             continue
1748         # A task the front released from the ready buffer is a move of its
1749         # own, about the task; the queue's next decision does not end it.
1750         feed = extra.get('action') == 'front_decision' and extra.get('outcome') == 'fed'
1751         subject = (json.dumps(['front_decision', extra.get('candidate')]) if feed
1752                    else _subject(r['instance_name'], extra))
1753         if subject in shown:
1754             continue
1755         if str(extra.get('outcome') or '') in FAILURE_OUTCOMES:
1756             key = failure_key(r['instance_name'], extra)
1757             if key not in last_ok:
1758                 last_ok[key] = _last_ok(r['instance_name'], extra, key)
1759             if last_ok[key] and last_ok[key] > r['timestamp']:
1760                 continue
1761         if not feed and _superseded(r['instance_name'], extra, r['timestamp']):
1762             continue
1763         shown.add(subject)
1764         if extra.get('action') == 'node_guard_decision':
1765             if ranks is None:
1766                 from monitor_app.panda.node_guard import exclusion_ranks
1767                 try:
1768                     ranks = exclusion_ranks()
1769                 except Exception:
1770                     logger.exception('live screen: node guard blacklist unreadable')
1771                     ranks = {}
1772             extra = site_node_guard_fields(extra, ranks)
1773         title, detail = board_text(r['message'], extra)
1774         if extra.get('action') == 'panda_task_operation' and extra.get('operation') == 'reassign':
1775             detail = _move_detail(extra, r['timestamp'], extra.get('task_name') or '')
1776         elif feed:
1777             title, detail = _feed_text(extra)
1778         items.append({'id': r['id'], 'at': r['timestamp'].isoformat(),
1779                       'level': r['levelname'], 'title': title, 'detail': detail,
1780                       'link': f"{PUBLIC}/logs/{r['id']}/"})
1781     return {'link': f'{PUBLIC}/logs/epicprod-live/',
1782             'numbering': 'site', 'items': items, 'hours': LIVE_WINDOW_H,
1783             'counts': _live_counts(cutoff)}
1784 
1785 
1786 def _live_counts(cutoff):
1787     """The live stream's records since ``cutoff`` at INFO, WARNING and
1788     ERROR, zero included, each with the logs page's list of exactly those
1789     records (COMP_SNAPSHOT.md, Live)."""
1790     from django.db.models import Count
1791     from monitor_app.epicprod_logging import live_stream_q
1792     from monitor_app.models import AppLog
1793     levels = ('INFO', 'WARNING', 'ERROR')
1794     found = dict(AppLog.objects.filter(live_stream_q(), timestamp__gte=cutoff,
1795                                        levelname__in=levels)
1796                  .values_list('levelname').annotate(n=Count('id')))
1797     return [{'level': level, 'count': found.get(level, 0),
1798              'link': (f'{PUBLIC}/logs/?app_name=epicprod&live=1&levelname={level}'
1799                       f'&hours={LIVE_WINDOW_H}')}
1800             for level in levels]
1801 
1802 
1803 def _coverage_gaps(scope, start):
1804     """Snapper's own record of the spans its collection missed since
1805     ``start``, ``[(gap start, gap end)]``: each gap a recovering snap closed
1806     (one whose start is unknown runs from the snap before it), and the open
1807     gap the capture cursor holds, to now."""
1808     from django.db.models import Q
1809     from snapper_ai.models import CaptureCursor, SystemSnap
1810 
1811     gaps = []
1812     rows = (SystemSnap.objects.filter(scope=scope, snap_time__gte=start)
1813             .filter(Q(recovered_gap_started_at__isnull=False)
1814                     | ~Q(recovered_gap_start_unknown=False))
1815             .values_list('snap_time', 'recovered_gap_started_at',
1816                          'recovered_gap_start_unknown'))
1817     for snap_time, gap_start, unknown in rows:
1818         if gap_start is None or unknown is not False:
1819             before = (SystemSnap.objects.filter(scope=scope, snap_time__lt=snap_time)
1820                       .order_by('-snap_time').values_list('snap_time', flat=True).first())
1821             gap_start = before or start
1822         gaps.append((gap_start, snap_time))
1823     cursor = CaptureCursor.objects.filter(scope=scope).first()
1824     if cursor is not None and cursor.coverage_gap_started_at:
1825         gaps.append((cursor.coverage_gap_started_at, timezone.now()))
1826     return sorted(gaps)
1827 
1828 
1829 def _trend_series(sites):
1830     """The floor's trend record (COMP_SNAPSHOT.md, Trends): the Snapper
1831     Site view's series product for the floor's live queues, with the axis
1832     of 48 whole quarter-hour bins ending at or before the product's own
1833     newest sample, and Snapper's coverage gaps over it. The task board's
1834     output flow shares the axis. None when no queue is live or the product
1835     holds no sample."""
1836     from snapper_ai.products import series_product
1837 
1838     live = [(s, [q['id'] for q in s['queues'] if q['status'] != 'not_built'])
1839             for s in sites]
1840     live = [(s, queues) for s, queues in live if queues]
1841     if not live:
1842         return None
1843     product = series_product('epicprod', 'site', window='24h',
1844                              selection=','.join(q for _, qs in live for q in qs))
1845     curves = product.get('curves') or {}
1846 
1847     def stamps(cid):
1848         return [datetime.fromisoformat(x) for x, _ in
1849                 (curves.get(cid) or {}).get('points') or []]
1850 
1851     # Each sample of a queue's PanDA state carries its finished counter, so
1852     # the counter's stamps are the queue's sample times.
1853     samples = {q: stamps(f'sjfw_{q}') for _, qs in live for q in qs}
1854     newest = max((x[-1] for x in samples.values() if x), default=None)
1855     if newest is None:
1856         return None
1857     step_s = TREND_BIN_MINUTES * 60
1858     bins = TREND_HOURS * 60 // TREND_BIN_MINUTES
1859     aligned = datetime.fromtimestamp(newest.timestamp() // step_s * step_s, dt_timezone.utc)
1860     edges = [aligned - timedelta(seconds=step_s * k) for k in range(bins, -1, -1)]
1861     gaps = [g for g in _coverage_gaps('epicprod', edges[0]) if g[1] >= edges[0]]
1862     return {'live': live, 'curves': curves, 'samples': samples, 'newest': newest,
1863             'edges': edges, 'gaps': gaps}
1864 
1865 
1866 def _trends(series):
1867     """The floor's trend plots (COMP_SNAPSHOT.md, Trends): per site with a
1868     live queue, on the trend record's axis, running and queued jobs at each
1869     bin's end and jobs finished and failed in the bin, summed over the
1870     site's live queues. A value is read at the queue's newest sample at or
1871     before the edge: a job state absent from that sample was zero then (the
1872     PanDA state lists only the states that hold jobs), and a sample older
1873     than TREND_STALE_MINUTES before the edge, or an edge inside a coverage
1874     gap, gives null, never a value carried forward. Finished and failed
1875     count up, so a bin's ended jobs are the difference of the count at its
1876     two ends, exact across a gap inside the bin, null where an end is
1877     unknown. The series rebases those counters by summing their nonnegative
1878     increments, so a raw counter reset reads as no increment, not as a
1879     detectable fall. A queue with no sample is null throughout."""
1880     from bisect import bisect_right
1881 
1882     from monitor_app.panda.census import NOT_STARTED
1883     from snapper_ai.models import CurrentComponent
1884 
1885     if series is None:
1886         return None
1887     curves, samples, edges, gaps = (series['curves'], series['samples'],
1888                                     series['edges'], series['gaps'])
1889     stale = timedelta(minutes=TREND_STALE_MINUTES)
1890 
1891     def values(cid):
1892         return {datetime.fromisoformat(x): v for x, v in
1893                 (curves.get(cid) or {}).get('points') or []}
1894 
1895     def at_edges(q, cid, absent_is_zero):
1896         """The curve at each edge, from the queue's newest sample at or
1897         before it."""
1898         times = samples.get(q) or []
1899         by_time = values(cid)
1900         out = []
1901         for edge in edges:
1902             i = bisect_right(times, edge)
1903             if not i or edge - times[i - 1] > stale or any(a <= edge <= b for a, b in gaps):
1904                 out.append(None)
1905                 continue
1906             v = by_time.get(times[i - 1])
1907             out.append(v if v is not None else (0 if absent_is_zero else None))
1908         return out
1909 
1910     def total(rows):
1911         return [None if any(v is None for v in col) else sum(col) for col in zip(*rows)]
1912 
1913     def ended(counts):
1914         # The counters are cumulative: a gap inside a bin loses none of its
1915         # ended jobs while the count at both ends is known. The series has
1916         # already rebased them (nonnegative increments), so b < a cannot
1917         # occur from a reset; it is guarded, not relied on.
1918         return [None if a is None or b is None or b < a else round(b - a)
1919                 for a, b in zip(counts, counts[1:])]
1920 
1921     def known_sum(xs):
1922         return None if any(v is None for v in xs) else sum(xs)
1923 
1924     def num(v):
1925         return 'unknown' if v is None else f'{v:,}'
1926 
1927     hour = 60 // TREND_BIN_MINUTES
1928     to = edges[-1].astimezone(ZoneInfo('America/New_York')).strftime('%H:%M')
1929     out = []
1930     for site, queues in series['live']:
1931         running = total([at_edges(q, f'sj_{q}_running', True) for q in queues])[1:]
1932         cores = total([at_edges(q, f'sjc_{q}', False) for q in queues])[1:]
1933         queued = total([at_edges(q, f'sj_{q}_{s}', True) for q in queues
1934                         for s in NOT_STARTED])[1:]
1935         finished = ended(total([at_edges(q, f'sjfw_{q}', False) for q in queues]))
1936         failed = ended(total([at_edges(q, f'sjxw_{q}', False) for q in queues]))
1937         name = short_name(site['name'])
1938         detail = (f'{name} at {to} ET: {num(cores[-1])} cores in {num(running[-1])} running jobs, {num(queued[-1])} queued jobs; '
1939                   f'{num(known_sum(finished[-hour:]))} finished, '
1940                   f'{num(known_sum(failed[-hour:]))} failed in the hour before')
1941         out.append({'id': site['id'], 'name': name, 'running': running,
1942                     'cores': cores,
1943                     'queued': queued, 'finished': finished, 'failed': failed,
1944                     'detail': detail})
1945     panda = CurrentComponent.objects.filter(scope='epicprod', name='panda').first()
1946 
1947     def iso(x):
1948         return x.astimezone(dt_timezone.utc).isoformat() if x else None
1949 
1950     return {
1951         'window_hours': TREND_HOURS, 'bin_minutes': TREND_BIN_MINUTES,
1952         't': [iso(x) for x in edges[1:]],
1953         'sampled_at': iso(series['newest']),
1954         'source_as_of': iso(panda.source_as_of) if panda else None,
1955         'gaps': [[iso(a), iso(b)] for a, b in gaps],
1956         'sites': out,
1957         'link': f'{PUBLIC}/snapper/epicprod/site/',
1958     }
1959 
1960 
1961 def clear_live_records(ids, reason, username, instance='mcp'):
1962     """Take records off the room's epicprod-live screen: one live_cleared
1963     action naming them and why (COMP_SNAPSHOT.md, Live). An id outside the
1964     epicprod stream is refused, never recorded."""
1965     from monitor_app.epicprod_logging import EPICPROD_APP_NAME, log_epicprod_action
1966     from monitor_app.models import AppLog
1967     ids = sorted({int(i) for i in ids or ()})
1968     reason = str(reason or '').strip()
1969     if not ids:
1970         raise ValueError('no record ids')
1971     if not reason:
1972         raise ValueError('a reason is required')
1973     found = set(AppLog.objects.filter(id__in=ids, app_name=EPICPROD_APP_NAME)
1974                 .values_list('id', flat=True))
1975     unknown = [i for i in ids if i not in found]
1976     if unknown:
1977         raise ValueError(f'not epicprod records: {unknown}')
1978     summary = (f"cleared {len(ids)} record(s) from the epicprod-live screen: "
1979                f"{', '.join(str(i) for i in ids)}; {reason}")
1980     row_id = log_epicprod_action(instance, 'live_cleared', username=username,
1981                                  message=summary, cleared_ids=ids,
1982                                  reason=reason, summary=summary)
1983     if row_id is None:
1984         raise RuntimeError('the live_cleared record was not written')
1985     return {'record_id': row_id, 'cleared_ids': ids, 'reason': reason}
1986 
1987 
1988 def _subject(instance_name, extra):
1989     """What a live record is about: its action on its subject, or for an
1990     action with no subject (a maintenance pass), the action from its
1991     source. The screen shows one record per subject, the newest."""
1992     if extra.get('subject_key'):
1993         return json.dumps([extra.get('action'), extra.get('subject_type'),
1994                            extra.get('subject_key')])
1995     return json.dumps([extra.get('action'), instance_name])
1996 
1997 
1998 def _superseded(instance_name, extra, at):
1999     """True when a later record of the same action on the same subject
2000     reports another outcome: a node guard decision the node has moved on
2001     from, a task operation followed by another. A later record with the
2002     same outcome (the node guard's hourly standing record) continues it."""
2003     from monitor_app.epicprod_logging import EPICPROD_APP_NAME
2004     from monitor_app.models import AppLog
2005     later = AppLog.objects.filter(app_name=EPICPROD_APP_NAME, timestamp__gt=at,
2006                                   extra_data__action=extra.get('action'))
2007     if extra.get('subject_key'):
2008         later = later.filter(extra_data__subject_type=extra.get('subject_type'),
2009                              extra_data__subject_key=extra['subject_key'])
2010     else:
2011         later = later.filter(instance_name=instance_name)
2012     if extra.get('outcome') is not None:
2013         later = later.exclude(extra_data__outcome=extra['outcome'])
2014     return later.exists()
2015 
2016 
2017 def _last_ok(instance_name, extra, key):
2018     """Time of the newest ok record carrying a failure's identity
2019     (live_notices.failure_key), or None: the recovery the epicprod-live
2020     channel announces."""
2021     from monitor_app.epicprod_logging import EPICPROD_APP_NAME
2022     from monitor_app.live_notices import failure_key
2023     from monitor_app.models import AppLog
2024     oks = AppLog.objects.filter(app_name=EPICPROD_APP_NAME,
2025                                 instance_name=instance_name,
2026                                 extra_data__action=extra.get('action'),
2027                                 extra_data__outcome='ok')
2028     for field in ('subject_type', 'subject_key', 'source', 'operation'):
2029         if extra.get(field) is not None:
2030             oks = oks.filter(**{f'extra_data__{field}': extra[field]})
2031     for ok in _pages(oks.order_by('-timestamp').values('timestamp', 'extra_data')):
2032         if failure_key(instance_name, ok['extra_data'] or {}) == key:
2033             return ok['timestamp']
2034     return None
2035 
2036 
2037 def _pages(queryset, size=100):
2038     """Rows of an ordered queryset in LIMIT/OFFSET pages: each page is an
2039     index scan, where a server-side cursor plans a sort of the whole set."""
2040     start = 0
2041     while True:
2042         page = list(queryset[start:start + size])
2043         yield from page
2044         if len(page) < size:
2045             return
2046         start += size
2047 
2048 
2049 def _jlab_pending():
2050     """What JLab Rucio is still owed: the failover stash's entries not yet
2051     home, from the drain's own record (the cached product ``stash_state``,
2052     swf-epicprod RUCIO_FAILOVER_STASH.md), and the registrations the
2053     registrar's last pass deferred, from its action record."""
2054     from monitor_app.models import AppLog, CachedProduct
2055     with connections['default'].cursor() as cursor:
2056         # counted in the database: after an outage the stash holds tens of
2057         # thousands of entries
2058         cursor.execute(f'''
2059             SELECT value ->> 'built_at',
2060                    (SELECT COUNT(*) FROM jsonb_array_elements(
2061                       CASE WHEN jsonb_typeof(value -> 'entries') = 'array'
2062                            THEN value -> 'entries' ELSE '[]'::jsonb END) e
2063                      WHERE e ->> 'outcome' IS DISTINCT FROM 'home')
2064             FROM {CachedProduct._meta.db_table} WHERE key = 'stash_state' ''')
2065         row = cursor.fetchone()
2066     stash, stash_at = (int(row[1]), row[0]) if row else (None, None)
2067     rec = (AppLog.objects.filter(app_name='epicprod', extra_data__action='registrar',
2068                                  extra_data__has_key='deferred')
2069            .order_by('-timestamp').values('timestamp', 'extra_data').first())
2070     deferred = int(rec['extra_data'].get('deferred') or 0) if rec else None
2071     known = [n for n in (stash, deferred) if n is not None]
2072     times = [t for t in (stash_at, rec['timestamp'].isoformat() if rec else None) if t]
2073     return {'pending': sum(known) if known else None,
2074             'parts': {'stash': stash, 'registrar': deferred},
2075             'at': min(times) if times else None}
2076 
2077 
2078 def _log_pending():
2079     """The logs pilots held in the stage-out bucket that are not yet in BNL
2080     Rucio, from the hourly log-rescue pass's record: those held over its
2081     window less those it moved (swf-epicprod LOG_STAGEOUT_FALLBACK.md,
2082     Recovery)."""
2083     from monitor_app.models import AppLog
2084     rec = (AppLog.objects.filter(app_name='epicprod', extra_data__action='log_rescue',
2085                                  extra_data__has_key='held')
2086            .order_by('-timestamp').values('timestamp', 'extra_data').first())
2087     if not rec:
2088         return None
2089     held = int(rec['extra_data'].get('held') or 0)
2090     moved = int(rec['extra_data'].get('moved') or 0)
2091     return {'pending': max(0, held - moved), 'parts': {'held': held, 'moved': moved},
2092             'at': rec['timestamp'].isoformat()}
2093 
2094 
2095 def _storage_capacity():
2096     """Each JLab disk RSE's usage against the production account's limit,
2097     from the latest storage component (swf-epicprod STORAGE.md, per RSE
2098     capacity), and the production output RSE's alone."""
2099     from monitor_app.snapper_storage import _current_data
2100     data = _current_data()
2101     if not data:
2102         raise LookupError('no storage component published')
2103     rses = []
2104     for name, r in sorted((data.get('rses') or {}).items()):
2105         c = (r or {}).get('capacity') or {}
2106         # the fraction is the account's usage over its limit where the
2107         # account figure is present, else the RSE-wide usage
2108         account = c.get('account_used') is not None
2109         rses.append({'id': name.lower(), 'name': name, 'type': (r or {}).get('type'),
2110                      'used_bytes': c.get('account_used') if account else c.get('used'),
2111                      'limit_bytes': c.get('limit'), 'fraction': c.get('fraction'),
2112                      'files': c.get('account_files') if account else c.get('files'),
2113                      'as_of': c.get('as_of')})
2114     output = next(({'rse': r['name'], 'used_bytes': r['used_bytes'], 'limit_bytes': r['limit_bytes'],
2115                     'fraction': r['fraction'], 'as_of': r['as_of']}
2116                    for r in rses if r['name'] == OUTPUT_RSE), None)
2117     if output:
2118         output['fill'] = _storage_fill(rses)
2119     return {'output': output, 'rses': rses}
2120 
2121 
2122 # A major store holds at least this share of the science data; the science
2123 # storage figure is amber when one is STORE_AMBER full, red at STORE_RED
2124 # (Torre Wenaus, 2026-10-29: "yellow when a major underlying store is 88%+,
2125 # red at 94%+").
2126 STORE_MAJOR_SHARE, STORE_AMBER, STORE_RED = 0.10, 0.88, 0.94
2127 
2128 
2129 def _storage_fill(rses):
2130     """The fullest major store and its level: red, amber or green."""
2131     held = [r for r in rses if r['used_bytes'] and r['fraction'] is not None]
2132     total = sum(r['used_bytes'] for r in held)
2133     major = [r for r in held if total and r['used_bytes'] / total >= STORE_MAJOR_SHARE]
2134     if not major:
2135         return None
2136     fullest = max(major, key=lambda r: r['fraction'])
2137     level = ('red' if fullest['fraction'] >= STORE_RED
2138              else 'amber' if fullest['fraction'] >= STORE_AMBER else 'green')
2139     return {'level': level, 'rse': fullest['name'], 'fraction': fullest['fraction']}
2140 
2141 
2142 FEED_SWITCH_REASONS = ('queue_off', 'queue_exempt')
2143 ET = ZoneInfo('America/New_York')
2144 
2145 
2146 def _subject_name(site, queue, live):
2147     """How a report line names what it is about: the site's display name,
2148     or the queue's short name where the site has more than one live queue."""
2149     return (short_name(site.get('name') or site['id']) if len(live) <= 1
2150             else short_name(queue['id']))
2151 
2152 
2153 def _when(at):
2154     """An ISO time as the report shows it, Eastern: '10/28 06:05 ET'."""
2155     if not at:
2156         return ''
2157     try:
2158         t = datetime.fromisoformat(str(at).replace('Z', '+00:00'))
2159     except ValueError:
2160         return str(at)
2161     if t.tzinfo is None:
2162         t = t.replace(tzinfo=dt_timezone.utc)
2163     return f'{t.astimezone(ET):%m/%d %H:%M} ET'
2164 
2165 
2166 def _account(when, by):
2167     """' (10/28 06:05 ET, swf-1)': when and who, as far as recorded."""
2168     bits = [b for b in (_when(when), by) if b]
2169     return f" ({', '.join(bits)})" if bits else ''
2170 
2171 
2172 def _site_state_items(value, cfg):
2173     """Every state a site is in, as report lines with who, when and why
2174     (COMP_SNAPSHOT.md, Report; Torre Wenaus, 2026-10-28: "absolute reported
2175     accountability in why a site is in the states it is in"), from the
2176     current settings: a front feed off or exempt, a core limit, the pool's
2177     fair-share limit and each production task paused at its queues."""
2178     items = []
2179     paused = (value.get('paused') or {}).get('items') or []
2180     for site in value.get('sites') or []:
2181         live = [q for q in site.get('queues', []) if q.get('status') == 'live']
2182         name = short_name(site.get('name') or site['id'])
2183         for q in live:
2184             if q.get('front') is None:
2185                 continue        # not a queue the front regulates
2186             base = f"front.queue.{q['id']}"
2187             if cfg.get(f'{base}.feed') is True:
2188                 continue
2189             subject = _subject_name(site, q, live)
2190             account = _account(cfg.get(f'{base}.feed_set_at'), cfg.get(f'{base}.feed_set_by'))
2191             exempt, off = cfg.get(f'{base}.feed_exempt'), cfg.get(f'{base}.feed_off_reason')
2192             if exempt:
2193                 items.append({'level': 'grey', 'site': site['id'], 'link': f'{PUBLIC}/panda/front/',
2194                               'key': f"feed_exempt:{q['id']}",
2195                               'text': f'{subject} front EXEMPT{account}: {exempt}'})
2196             else:
2197                 items.append({'level': 'red', 'site': site['id'], 'link': f'{PUBLIC}/panda/front/',
2198                               'key': f"feed_off:{q['id']}",
2199                               'text': (f'{subject} front feed OFF{account}: {off}' if off else
2200                                        f'{subject} front feed OFF: no reason recorded')})
2201         for control in (site.get('worker_limit') or {}).get('controls') or []:
2202             subject = _subject_name(site, {'id': control['queue']}, live)
2203             cores = (f"{control['core_limit']:,} cores" if control.get('core_limit') is not None
2204                      else f"{control.get('limit')} allocations")
2205             items.append({'level': 'red', 'site': site['id'], 'key': f"limit:{control['queue']}",
2206                           'link': (site.get('links') or {}).get('panda_queue'),
2207                           'text': (f'{subject} core limit {cores}'
2208                                    f"{_account(control.get('at'), control.get('by'))}: "
2209                                    f"{control.get('reason') or 'no reason recorded'}")})
2210         # A whole-node queue past its node ceiling (Torre Wenaus, 2026-10-28:
2211         # "5 IS OUR MAX"): the Harvester limit is the ceiling's enforcement,
2212         # this line its check.
2213         ceilings = _node_ceilings()
2214         for q in site.get('queues', []):
2215             w = q.get('workers_now') or {}
2216             nodes = int(w.get('running') or 0) + int(w.get('submitted') or 0)
2217             if ceilings.get(q['id']) and nodes > ceilings[q['id']]:
2218                 items.append({'level': 'red', 'site': site['id'], 'key': f"nodes:{q['id']}",
2219                               'link': (site.get('links') or {}).get('panda_queue'),
2220                               'text': (f"{_subject_name(site, q, live)} nodes {nodes} over the "
2221                                        f"ceiling of {ceilings[q['id']]} ({w.get('running') or 0} "
2222                                        f"running, {w.get('submitted') or 0} waiting)")})
2223         notice = (site.get('fair_share') or {}).get('notice')
2224         if notice:
2225             items.append({'level': notice.get('level') or 'amber', 'site': site['id'],
2226                           'key': f"fair_share:{site['id']}", 'link': notice.get('link'),
2227                           'text': f"{name} {notice['text']}: {notice.get('detail') or ''}".rstrip(': ')})
2228         queues = {q['id'] for q in site.get('queues', [])}
2229         for task in paused:
2230             if task.get('queue') not in queues:
2231                 continue
2232             subject = _subject_name(site, {'id': task['queue']}, live)
2233             # The task guard's pause is failing work: red. A person's pause
2234             # with its reason is information: grey (Torre Wenaus, 2026-10-28:
2235             # "not a red condition! it is info"). One outside our tools has
2236             # no reason anyone recorded: red.
2237             by = task.get('paused_by')
2238             level = 'red' if by in ('front task guard', 'outside our tools') or not task.get('pause_reason') else 'grey'
2239             items.append({'level': level, 'site': site['id'], 'link': task.get('link'),
2240                           'key': f"paused:{task['id']}",
2241                           'text': (f"{subject} task {task['id']} paused"
2242                                    f"{_account(task.get('since'), task.get('paused_by'))}: "
2243                                    f"{task.get('pause_reason') or 'no reason recorded'}")})
2244     return items
2245 
2246 
2247 NUMBERS_KEY = 'prod_now_numbers'
2248 NUMBER_HOLD = timedelta(hours=1)
2249 NUMBER_SEEN_REFRESH = timedelta(minutes=5)
2250 
2251 
2252 def _stable_numbers(keys, now=None):
2253     """{key: (n, since)}. A Production now line's number is immutable
2254     (Torre Wenaus, 2026-10-28: "prod now numbering should be immutable"):
2255     a new line takes the next number of the PersistentState counter
2256     ``prod_now_line``, which only rises, so a number names one line and is
2257     never given to another, and the newest line has the biggest. A line
2258     keeps its number while it stands, and a cleared line's number stays
2259     held for NUMBER_HOLD, so a line that returns within it keeps it; one
2260     that returns later takes a new number. ``since`` is when the line first
2261     stood. The lines are held in the CachedProduct row NUMBERS_KEY,
2262     ``{key: {n, seen, since}}``, written only when it changes."""
2263     from django.db import transaction
2264     from monitor_app.models import CachedProduct, PersistentState
2265     now = now or timezone.now()
2266     with transaction.atomic():
2267         row, _ = CachedProduct.objects.select_for_update().get_or_create(
2268             key=NUMBERS_KEY, defaults={'value': {}})
2269         held = dict(row.value or {})
2270         changed = False
2271         # the counter's first number is above every number already held
2272         start = max((entry['n'] for entry in held.values()), default=0) + 1
2273         for key, entry in list(held.items()):
2274             if key not in keys and now - datetime.fromisoformat(entry['seen']) > NUMBER_HOLD:
2275                 del held[key]
2276                 changed = True
2277         for key in keys:
2278             if key in held:
2279                 if now - datetime.fromisoformat(held[key]['seen']) > NUMBER_SEEN_REFRESH:
2280                     held[key] = dict(held[key], seen=now.isoformat())
2281                     changed = True
2282                 if 'since' not in held[key]:
2283                     # a line numbered before its first-stood time was kept
2284                     held[key] = dict(held[key], since=now.isoformat())
2285                     changed = True
2286                 continue
2287             held[key] = {'n': PersistentState.next_number('prod_now_line', start),
2288                          'seen': now.isoformat(), 'since': now.isoformat()}
2289             changed = True
2290         if changed:
2291             row.value = held
2292             row.save(update_fields=['value', 'updated_at'])
2293     return {key: (held[key]['n'], held[key]['since']) for key in keys}
2294 
2295 
2296 def _numbered(items, sites):
2297     """The report's lines newest first, the biggest number at the top
2298     (Torre Wenaus, 2026-10-28: "reverse time order, which means I should
2299     see biggest numbers at top"), with their standing numbers
2300     (``_stable_numbers``), the colour carrying the level; each site's own lines as
2301     ``site.notices`` for its panel, and its bullet ``site.level``: the
2302     worst of its health, status and lines."""
2303     for item in items:
2304         item.setdefault('key', item.get('text') or '')
2305     try:
2306         numbers = _stable_numbers(list(dict.fromkeys(i['key'] for i in items)))
2307     except Exception:
2308         logger.exception('Production now numbers unavailable')
2309         numbers = {}
2310     for item in items:
2311         item['n'], item['since'] = numbers.get(item['key'], (None, None))
2312     # Newest first: the biggest number at the top, numbers rising with time.
2313     items = sorted(items, key=lambda i: (i['n'] is not None, i['n'] or 0), reverse=True)
2314     by_site = {}
2315     for item in items:
2316         if item.get('site'):
2317             by_site.setdefault(item['site'], []).append(
2318                 {k: item.get(k) for k in ('n', 'level', 'text', 'link')})
2319     for site in sites:
2320         site['notices'] = by_site.get(site['id'], [])
2321         health = (site.get('health') or {}).get('level')
2322         if site.get('status') == 'live':
2323             site['level'] = _worst([health, site.get('status_level')]
2324                                    + [i['level'] for i in site['notices']])
2325         else:
2326             site['level'] = site.get('status_level') or health
2327     return items
2328 
2329 
2330 def _guard_reason(evidence):
2331     """The front's task guard's trip, from its pause evidence."""
2332     e = evidence or {}
2333     if e.get('trigger') == 'consecutive':
2334         return f"{e.get('consecutive_failed')} jobs failed on {e.get('run_hosts')} hosts"
2335     if e.get('trigger'):
2336         return (f"{e.get('window_failed')} of {e.get('counted')} jobs failed "
2337                 f"on {e.get('window_hosts')} hosts")
2338     if e.get('failed') is not None:
2339         return f"{e['failed']} of {int(e.get('finished') or 0) + int(e['failed'])} jobs failed"
2340     return None
2341 
2342 
2343 def _pause_accounts(task_ids):
2344     """{jedi task id: (who, why)} from each task's latest pause made
2345     through our tools (PandaTaskOperation); PanDA records no reason."""
2346     from monitor_app.models import PandaTaskOperation
2347     out = {}
2348     if not task_ids:
2349         return out
2350     for op in (PandaTaskOperation.objects.filter(jedi_task_id__in=list(task_ids), operation='pause')
2351                .order_by('jedi_task_id', '-requested_at')):
2352         if op.jedi_task_id in out:
2353             continue
2354         evidence = op.evidence or {}
2355         if str(op.source or '').startswith('front_task_failure_guard'):
2356             out[op.jedi_task_id] = ('front task guard',
2357                                     _guard_reason(evidence) or evidence.get('reason'))
2358         else:
2359             out[op.jedi_task_id] = (op.requested_by or None,
2360                                     evidence.get('reason') or evidence.get('note'))
2361     # A pause with no record was made outside our tools, in PanDA itself,
2362     # which keeps no reason (EPICPROD_OPS.md: no pause without a reason).
2363     for task_id in task_ids:
2364         out.setdefault(task_id, ('outside our tools',
2365                                  'paused directly in PanDA, which records no reason'))
2366     return out
2367 
2368 
2369 def _report_items(front, sites, storage, alarms, assessment):
2370     """The report as items (docs/COMP_SNAPSHOT.md, Report): what the room's
2371     floor cannot show, each one line with a level and the page to open.
2372     No per-site counts: the pillars carry those."""
2373     items = []
2374     if not front.get('enabled'):
2375         items.append({'level': 'red', 'key': 'front:off', 'text': 'Pressure front: off, nothing is fed',
2376                       'link': front.get('link')})
2377     for s in sites:
2378         live = [q for q in s['queues'] if q['status'] == 'live']
2379         if not live:
2380             continue
2381         for q in live:
2382             subject = _subject_name(s, q, live)
2383             f = q.get('front') or {}
2384             level, words = FRONT_STATES.get(f.get('state'), ('grey', f.get('state') or ''))
2385             # A feed switch is the site's own state line, with who, when
2386             # and why, composed when the record is read (_site_state_items).
2387             if (f.get('state') and f.get('state') not in ('supplied', 'fed')
2388                     and f.get('reason') not in FEED_SWITCH_REASONS):
2389                 reason = FRONT_REASONS.get(f.get('reason'), '')
2390                 items.append({'level': level, 'site': s['id'], 'key': f"front:{q['id']}",
2391                               'text': f"Front, {subject}: {words}{', ' + reason if reason else ''}",
2392                               'link': front.get('link')})
2393             for name, p in q['health']['parts'].items():
2394                 if p['level'] not in ('amber', 'red'):
2395                     continue
2396                 link = (q['links'].get('slots') if name == 'pilot_supply' else
2397                         q['links'].get('canary') if name == 'canary' else
2398                         q['links'].get('snapper'))
2399                 items.append({'level': p['level'], 'site': s['id'], 'key': f"health:{q['id']}:{name}",
2400                               'text': f"{subject} {PART_NAMES.get(name, name)}: {p['note']}",
2401                               'link': link})
2402     seen = set()
2403     for a in alarms or []:  # newest first
2404         title = a['title']
2405         # one line per task: the failure-rate alarms fire per task, and
2406         # several alarms (and older events) can name the same one
2407         task = TASK_ALARM_RE.match(title)
2408         key = ('task', task.group(1)) if task else ('title', title)
2409         if key in seen:
2410             continue
2411         seen.add(key)
2412         # The alarm engine selects approaching pings by their lead time.
2413         # Their recorded title's countdown ages, and an overdue ping keeps
2414         # it at severity alarm; display the due date.
2415         ping = a.get('severity') == 'ping'
2416         due = DUE_RE.match(title) if not task else None
2417         if task:
2418             number, state, rate, name = task.groups()
2419             text = f'Final failures {rate}, task {number} ({state}): {name}'
2420             key = f'alarm:task:{number}'
2421         elif due:
2422             date, what = due.groups()
2423             text = f"{'Due' if ping else 'Overdue, due'} {date}: {what}"
2424             key = f'due:{date}:{what}'
2425         else:
2426             text = f"{'Due' if ping else 'Alarm'}: {title.replace(' — ', ': ')}"
2427             key = f'alarm:{title}'
2428         items.append({'level': 'amber' if ping else 'red', 'text': text, 'key': key,
2429                       'link': f"{PUBLIC}/alarms/events/{a['id']}/"})
2430     for st in storage:
2431         for name, p in st['health']['parts'].items():
2432             if p['level'] in ('amber', 'red'):
2433                 items.append({'level': p['level'], 'key': f"storage:{st['id']}:{name}",
2434                               'text': f"{st['name']}: {p['note']}",
2435                               'link': (st.get('links') or {}).get('storage')})
2436     if assessment:
2437         level = {'ok': 'green', 'green': 'green', 'warn': 'amber', 'warning': 'amber',
2438                  'amber': 'amber', 'fail': 'red', 'error': 'red', 'red': 'red'}.get(
2439                      assessment['status'], 'grey')
2440         if level in ('amber', 'red'):
2441             items.append({'level': level, 'key': 'assessment',
2442                           'text': f"Campaign assessment: {assessment['summary']}",
2443                           'link': f'{PUBLIC}/ai/assessments/?subject_type=campaign_task,campaign'})
2444     items.sort(key=lambda i: LEVEL_ORDER.get(i['level'], 2))
2445     return items
2446 
2447 
2448 def incomplete_registration_link(observed_at):
2449     """The same completion window and registration subset as the count."""
2450     if isinstance(observed_at, str):
2451         observed_at = datetime.fromisoformat(observed_at)
2452     return f'{PUBLIC}/panda/jobs/?' + urlencode({
2453         'status': 'finished', 'registration': 'incomplete', 'hours': 1,
2454         'ended_after': (observed_at - timedelta(hours=1)).isoformat(),
2455         'ended_before': observed_at.isoformat(),
2456     })
2457 
2458 
2459 def _dashboard_campaign():
2460     """Current campaign, from the same cached completion product as Nav."""
2461     from pcs.models import Campaign
2462     from monitor_app.cached_product import get_product
2463     from swf_epicprod.analytics.completion import completion_product_value
2464 
2465     name = Campaign.objects.filter(lifecycle='current').values_list('name', flat=True).first()
2466     if not name:
2467         return None
2468     product = get_product('prod_hub_campaign_completion', completion_product_value,
2469                           ttl_seconds=600)
2470     row = next((row for row in product['value'] or [] if row['campaign'] == name), {})
2471     totals = row.get('overall') or {}
2472     return {'id': name, 'delivered_since': totals.get('delivered_since'),
2473             'events': totals.get('delivered_events'), 'bytes': totals.get('bytes'),
2474             'configurations': totals.get('configurations')}
2475 
2476 
2477 def _gke_workers(cfg, queue='BNL_ePIC_GOOGLE_es'):
2478     """The GKE controller's fresh worker reading for its queue, its age, and
2479     the pilot-supply part it gives (swf-epicprod GKE_PILOT_FLOW.md): the
2480     controller, not Harvester, supplies this queue."""
2481     from monitor_app.models import CachedProduct
2482     from swf_epicprod.gke_pilots import CAPABILITY_KEY
2483 
2484     row = CachedProduct.objects.filter(key=CAPABILITY_KEY).first()
2485     cap = (row.value or {}) if row else {}
2486     age = ((timezone.now() - row.built_at).total_seconds()
2487            if row and row.built_at else None)
2488     fresh = (age is not None and 0 <= age <= REPORT_STALE_SECONDS
2489              and cap.get('queue') == queue)
2490     workers = cap.get('workers') if fresh else None
2491     if workers is not None and (workers.get('error') or not all(
2492             isinstance(workers.get(key), int) for key in ('pending', 'running'))):
2493         workers = None
2494     latest = (workers or {}).get('latest') or {}
2495     state = 'red' if latest.get('failed') else 'green' if workers is not None else 'amber'
2496     text = (f"{workers['pending']} pilots waiting, {workers['running']} running"
2497             if workers is not None else 'pilot supply unknown')
2498     return workers, age, _part(state, None, text)
2499 
2500 
2501 def _google_pilot_display(site, cfg):
2502     """Supply counts for the ordinary production presentation; diagnostics stay in the controller record."""
2503     queue = next((q for q in site['queues'] if q['id'] == 'BNL_ePIC_GOOGLE_es'), None)
2504     if not queue or not queue.get('production_display'):
2505         return
2506     limit = int(cfg.get('gke_pilots.max_pods') or 0)
2507     workers, age, workers_part = _gke_workers(cfg, queue['id'])
2508     if workers is not None:
2509         pending, running = workers.get('pending'), workers.get('running')
2510         queue['pilots'] = {'report': 'gke-pilots', 'waiting': pending,
2511                            'waiting_current': pending, 'waiting_source': 'gke-pilots',
2512                            'running': running, 'limit': limit,
2513                            'per_slot': 1, 'report_age_seconds': round(age, 1),
2514                            'report_stale_seconds': REPORT_STALE_SECONDS,
2515                            'limit_source': 'controller', 'error': None}
2516     else:
2517         queue['pilots'] = None
2518     status_level = 'green' if queue['status'] == 'live' else 'blue' if queue['status'] == 'commissioning' else 'grey'
2519     queue['health'] = _health({'status': _part(status_level, queue['status'], queue['status']),
2520                                'gke_workers': workers_part})
2521     queue['status_reason'] = queue['status']
2522     site['health'] = {'level': _worst(q['health']['level'] for q in site['queues']),
2523                       'parts': {q['id']: q['health']['level'] for q in site['queues']}}
2524     site['status_reason'] = queue['status']
2525 
2526 
2527 def _report_header(record):
2528     """Display-ready header blocks shared by the dashboard and room."""
2529     queues = [q for site in record.get('sites', []) for q in site.get('queues', [])]
2530 
2531     def total(key):
2532         return (sum(q[key] for q in queues)
2533                 if queues and all(q.get(key) is not None for q in queues) else None)
2534 
2535     def number(value, divisor=1, suffix=''):
2536         return f'{int(value / divisor + 0.5):,}{suffix}' if value is not None else '—'
2537 
2538     def metric(value, label, colour, link, detail):
2539         return {'value': value, 'label': label, 'colour': colour, 'link': link, 'detail': detail}
2540 
2541     jobs = f'{PUBLIC}/panda/jobs/'
2542     front = f'{PUBLIC}/panda/front/#ready'
2543     campaign = record.get('campaign') or {}
2544     plan = f"{PUBLIC}/pcs/plan/?campaign={quote(campaign.get('id') or '')}"
2545     thread_count = total('processing_threads')
2546     thread_value = number(thread_count)
2547     thread_detail = 'Cores carrying payload work in running production jobs'
2548     known_threads = total('processing_known_threads')
2549     if thread_count is None and known_threads:
2550         thread_value = number(known_threads)
2551         thread_detail += '; lower bound because some jobs have no fresh payload-thread measurement'
2552     since = campaign.get('delivered_since')
2553     if since:
2554         date = datetime.fromisoformat(str(since))
2555         since = f"{date.strftime('%b')} {date.day}"
2556     return [
2557         {'id': 'computing', 'title': 'Computing now', 'link': f'{PUBLIC}/panda/compute-efficiency/',
2558          'detail': 'Compute efficiency history by site and month or week.',
2559          'metrics': [metric(thread_value, 'cores', '#edf2f1',
2560                             jobs + '?status=running&days=2', thread_detail),
2561                      metric(number(total('finished_cores_1h')), 'finish/1h', '#7fce95',
2562                             jobs + '?status=finished&days=1', 'Core completions in the last hour')]},
2563         {'id': 'work', 'title': 'Work available', 'link': front,
2564          'detail': 'Queued jobs and input work not yet generated into jobs.',
2565          'metrics': [metric(number(total('queued')), 'queued', '#a79b8f',
2566                             jobs + '?status=queued&days=30', 'Jobs waiting to start'),
2567                      metric(number(total('ungenerated')), 'to generate', '#9cabb5',
2568                             front, 'Input work not yet generated into jobs')]},
2569         {'id': 'campaign', 'title': 'Campaign ' + (campaign.get('id') or '—'),
2570          'subtitle': 'delivered since ' + (since or '—'), 'link': plan,
2571          'detail': 'Campaign delivery recorded in the production plan.',
2572          'metrics': [metric(number(campaign.get('events'), 1e6, 'M'), 'events', '#7fce95',
2573                             plan, 'Delivered events'),
2574                      metric(number(campaign.get('bytes'), 1e12), 'TB', '#7fce95',
2575                             plan, 'Delivered data in decimal terabytes')],
2576          'footer': {'text': number(campaign.get('configurations')) + ' physics configs',
2577                     'link': plan, 'detail': 'Physics configurations in the current campaign'}},
2578     ]
2579 
2580 
2581 def get_snapshot(refresh=False):
2582     """The stored record, rebuilt behind the request when older than the
2583     TTL (docs/CACHED_PRODUCTS.md)."""
2584     from monitor_app.cached_product import get_product
2585     from monitor_app.models import SysConfig
2586     from swf_epicprod.regulation import PILOTS_PER_WORKER
2587     result = get_product(PRODUCT_KEY, build, TTL_SECONDS, refresh=refresh)
2588     value = dict(result.get('value') or {})
2589     # Correct the old queue-wide labels immediately, without rebuilding
2590     # the cached census, storage or histories.
2591     if value.get('live') and value['live'].get('numbering') != 'site':
2592         value['live'] = _live_records()
2593     # Labels follow the current shared vocabulary even while counts are cached.
2594     value['sites'] = [dict(site, execute_sites=[
2595         dict(item, short=short_name(item.get('name')))
2596         for item in site.get('execute_sites', [])
2597     ]) for site in value.get('sites', [])]
2598     status_by_queue = {q['id']: q for s in _sites_config() for q in s.get('queues', [])}
2599     cfg = SysConfig.get_config()
2600     for site in value['sites']:
2601         site['name'] = short_name(site.get('name') or site['id'])
2602         site['queues'] = [dict(q, pilots=_pilot_display(q.get('pilots')))
2603                           for q in site.get('queues', [])]
2604         for queue in site['queues']:
2605             declared = status_by_queue.get(queue['id']) or {}
2606             if declared.get('status'):
2607                 queue['status'] = declared['status']
2608             queue['production_display'] = _production_display(queue['id'], queue['status'], cfg)
2609             queue['status_reason'] = short_text(queue.get('status_reason'))
2610             queue['health'] = dict(queue['health'], parts={
2611                 key: dict(part, note=short_text(part.get('note')))
2612                 for key, part in queue['health']['parts'].items()})
2613         primary = _primary_queue(site['queues'])
2614         site['status'] = primary['status'] if primary else 'off'
2615         site['status_label'] = {
2616             'live': '', 'commissioning': 'COMMISSIONING', 'off': 'OFF',
2617             'not_built': 'OFF', 'not_live': 'OFF',
2618         }.get(site['status'], site['status'].replace('_', ' ').upper())
2619         if site['status'] != 'live':
2620             site['status_label'] = INACTIVE_SITE_LABELS.get(site['id'], site['status_label'])
2621         else:
2622             site['status_label'] = LIVE_SITE_LABELS.get(site['id'], '')
2623         site['status_level'] = (
2624             'grey' if site['id'] in INACTIVE_SITE_LABELS and site['status'] != 'live' else
2625             'blue' if site['status'] == 'commissioning' else
2626             None if site['status'] == 'live' else 'grey')
2627         site['production_panel'] = True
2628         site['status_reason'] = (primary or {}).get('status_reason') or site['status_label']
2629         # A live site whose front feed an operator switched off, with the
2630         # reason recorded, says so in its panel: running work continues,
2631         # the front sends no more.
2632         off = [q['id'] for q in site['queues']
2633                if cfg.get(f"front.queue.{q['id']}.feed") is False
2634                and cfg.get(f"front.queue.{q['id']}.feed_off_reason")]
2635         if site['status'] == 'live' and off:
2636             site['status_label'] = 'Pressure front OFF'
2637             site['status_level'] = 'red'
2638             site['status_reason'] = (f"{', '.join(off)}: the pressure front's feed is off; running work "
2639                                      f"continues. {cfg.get(f'front.queue.{off[0]}.feed_off_reason')}")
2640         if primary:
2641             site['links'] = dict(primary['links'])
2642         limits = []
2643         for queue in site['queues']:
2644             control = _worker_limit(cfg, queue['id'])
2645             if control:
2646                 width = control.get('cores_per_worker') or PILOTS_PER_WORKER.get(queue['id'])
2647                 core_limit = control['limit'] * width if isinstance(width, int) and width > 0 else None
2648                 limits.append(dict(control, queue=queue['id'], core_limit=core_limit))
2649         site['worker_limit'] = None
2650         if limits:
2651             levels = ', '.join(f"{item['core_limit']:,}" if item['core_limit'] is not None
2652                                else 'unavailable' for item in limits)
2653             detail = '\n'.join(
2654                 f"{item['queue']}: {item['core_limit'] if item['core_limit'] is not None else 'unknown'} "
2655                 f"core ceiling ({item['limit']:,} Harvester allocations); "
2656                 f"{item.get('reason') or 'operator override'}. "
2657                 'Existing allocations finish normally.' for item in limits)
2658             site['worker_limit'] = {
2659                 'active': True, 'controls': limits,
2660                 'notice': {'text': f'Core limit {levels}', 'level': 'red',
2661                            'detail': detail,
2662                            'link': (site.get('links') or {}).get('panda_queue')},
2663             }
2664         if site['id'] == 'google':
2665             site.pop('board', None)
2666             _google_pilot_display(site, cfg)
2667         if isinstance(site.get('fair_share'), dict):
2668             share = site['fair_share']
2669             site['fair_share'] = dict(share, notice=_fair_share_notice(
2670                 share, (site.get('links') or {}).get('panda_queue')))
2671     from monitor_app.processing_activity import product as processing_product
2672     try:
2673         activity = processing_product()
2674         measured = activity.get('value') or {}
2675         # The last measured counts stand until the measurement refreshes; a
2676         # count is never blanked (processing_activity holds a queue's last
2677         # complete count while any of its jobs is unmeasured).
2678         # An Event Service queue's production shows as events, not finished
2679         # jobs: its jobs finish once in hours (PROD_DASHBOARD.md).
2680         from swf_epicprod.es_closeout import setting as es_setting
2681         es_queues = set(es_setting('queues', seed=False) or ())
2682         for site in value['sites']:
2683             for queue in site['queues']:
2684                 row = (measured.get('queues') or {}).get(queue['id'], {})
2685                 queue['processing_threads'] = row.get('threads', 0)
2686                 queue['processing_known_threads'] = row.get('known_threads', 0)
2687                 queue['processing_unknown_jobs'] = row.get('unknown_jobs', 0)
2688                 queue['processing_threads_at'] = row.get('threads_at')
2689                 queue['event_service'] = queue['id'] in es_queues
2690                 queue['events_1h'] = row.get('events_1h', 0)
2691                 queue['events_unknown_jobs'] = row.get('events_unknown_jobs', 0)
2692             counts = [q['processing_threads'] for q in site['queues']]
2693             site['processing_threads'] = sum(counts) if all(n is not None for n in counts) else None
2694             for executor in site.get('execute_sites', []):
2695                 matches = [r for name, r in (measured.get('executors') or {}).items()
2696                            if (_uncut_site(name) or 'not reported') == executor['name']]
2697                 counts = [r.get('threads') for r in matches]
2698                 executor['processing_threads'] = (sum(counts) if all(n is not None for n in counts)
2699                                                   else sum(r['known_threads'] for r in matches))
2700         value['processing_observed_at'] = measured.get('observed_at')
2701     except Exception:
2702         logger.exception('Payload processing activity unavailable')
2703         for site in value['sites']:
2704             site['processing_threads'] = None
2705             for queue in site['queues']:
2706                 queue['processing_threads'] = None
2707     trends = value.get('trends')
2708     if trends:
2709         trends = dict(trends, sites=[dict(s, name=short_name(s.get('name')))
2710                                     for s in trends.get('sites', [])])
2711         value['trends'] = trends
2712         known = {s['id'] for s in trends.get('sites', [])}
2713         missing = [s for s in value['sites'] if s['id'] not in known]
2714         if missing:
2715             empty = [None] * len(trends.get('t') or [])
2716             value['trends'] = dict(trends, sites=[*trends.get('sites', []), *(
2717                 {'id': s['id'], 'name': s['name'], 'detail': 'No trend samples yet',
2718                  **{key: list(empty) for key in ('running', 'queued', 'finished', 'failed')}}
2719                 for s in missing)])
2720     if value.get('observed_at'):
2721         value['storage'] = [dict(row, links=dict(row.get('links') or {},
2722             failed=incomplete_registration_link(value['observed_at'])))
2723             if row.get('id') == 'jlab-rucio' else row
2724             for row in value.get('storage', [])]
2725     from monitor_app.site_core_metrics import product as core_product, ready_capacity
2726     try:
2727         core_result = core_product()
2728         core_data = core_result.get('value') or {}
2729         # The last complete accounting stands until the next one replaces it
2730         # (PROD_DASHBOARD.md, never blank): the product rebuilds behind a
2731         # read, so the first read after the dashboard sat unattended finds it
2732         # old, and blanking then dropped every curve but running.
2733         core_fresh = bool(core_data.get('since'))
2734         google = core_data.get('google') or {}
2735         for site in value['sites']:
2736             for queue in site['queues']:
2737                 row = (core_data.get('queues') or {}).get(queue['id'], {})
2738                 for key in ('finished_cores_1h', 'failed_cores_1h', 'queued_cores',
2739                             'finished_jobs_1h', 'failed_jobs_1h'):
2740                     queue[key] = row.get(key, 0) if core_fresh else None
2741                 queue['queued_jobs'] = row.get('queued_jobs', 0) if core_fresh else None
2742                 if queue['id'] == google.get('queue'):
2743                     site['node_cap'] = google['nodes']
2744                     site['cores_per_node'] = google['cores_per_node']
2745                     queue['queued_core_demand'] = row.get('queued_core_demand', 0)
2746                 if queue['id'] == perlmutter_production.QUEUE:
2747                     # The nodes running now, from PanDA's worker table, so the
2748                     # title corrects itself (Torre Wenaus, 2026-10-28: "title
2749                     # should be auto correct"), and each node's work lanes
2750                     # With no worker reading (an older record, a failed
2751                     # read), the running jobs, one per node: never a false 0.
2752                     workers_now = queue.get('workers_now')
2753                     site['node_cap'] = (int(workers_now.get('running') or 0) if workers_now
2754                                         else int(queue.get('running') or 0))
2755                     site['cores_per_node'] = int(perlmutter_production.SHAPE['slots'])
2756             trend = next((t for t in (value.get('trends') or {}).get('sites', [])
2757                           if t['id'] == site['id']), None)
2758             if not trend:
2759                 continue
2760             ids = [q['id'] for q in site['queues']]
2761             times = value['trends']['t']
2762             for state in ('finished', 'failed'):
2763                 trend[f'{state}_cores'] = [
2764                     sum((core_data.get('ended', {}).get(stamp, {}).get(q, {}).get(state, 0)) for q in ids)
2765                     if core_fresh and stamp >= core_data['since'] else None for stamp in times]
2766             if not core_fresh:
2767                 trend['queued_cores'] = [None] * len(times)
2768             elif site['id'] == 'google' and google:
2769                 trend['queued_cores'] = [ready_capacity(n, google['nodes'], google['cores_per_node'])
2770                                          if n is not None else None for n in trend['queued']]
2771             elif perlmutter_production.QUEUE in ids:
2772                 # A waiting whole-node job counts its node's 128 work lanes,
2773                 # as the panel's figure does (site_core_metrics); the non-ES
2774                 # queue takes no production.
2775                 lanes = int(perlmutter_production.SHAPE['slots'])
2776                 trend['queued_cores'] = [n * lanes if n is not None else None
2777                                          for n in trend['queued']]
2778             else:
2779                 # The native OSG/GREX queues use one core per queued job.
2780                 trend['queued_cores'] = list(trend['queued'])
2781             # The plot's live right end: the panel's own figures now, so the
2782             # number and the curve's end agree (PROD_DASHBOARD.md).
2783             trend['live'] = {'cores': site.get('processing_threads'),
2784                              'queued_cores': (sum(q.get('queued_cores') or 0 for q in site['queues'])
2785                                               if core_fresh else None)}
2786         if value.get('trends'):
2787             value['trends']['live_t'] = value.get('processing_observed_at') or value.get('observed_at')
2788     except Exception:
2789         logger.exception('Site core accounting unavailable')
2790     try:
2791         value['campaign'] = _dashboard_campaign()
2792     except Exception:
2793         logger.exception('Dashboard campaign summary unavailable')
2794         value['campaign'] = None
2795     value['report'] = dict(value.get('report') or {}, header=_report_header(value),
2796                           text=_report_text(value.get('pressure_front') or {}, value['sites'],
2797                                             value.get('storage') or []))
2798     # Each state a site is in is a line, every line is numbered, and each
2799     # site carries its lines and its bullet (COMP_SNAPSHOT.md, Report).
2800     try:
2801         state_items = _site_state_items(value, cfg)
2802     except Exception:
2803         logger.exception('Site state lines unavailable')
2804         state_items = [{'level': 'red', 'key': 'error:state',
2805                         'text': 'Site state lines unavailable: see the monitor log'}]
2806     value['report']['items'] = _numbered(
2807         [dict(item, text=short_text(item.get('text')))
2808          for item in value['report'].get('items', []) + state_items], value['sites'])
2809     value['age_seconds'] = result.get('age_seconds')
2810     value['refreshing'] = result.get('refreshing')
2811     return value