| 67 |
67 |
STATUS_DEGRADED = "degraded" |
| 68 |
68 |
STATUS_BROKEN = "broken" |
| 69 |
69 |
STATUS_STALE = "stale" |
| 70 |
|
−ALL_STATUSES = (STATUS_OK, STATUS_DEGRADED, STATUS_BROKEN, STATUS_STALE) |
|
70 |
+# Source retirée de la supervision active : désactivée explicitement par |
|
71 |
+# l'app (``disabled_stores``/``disabled_sources`` du /api/stats) ou disparue |
|
72 |
+# de toute fenêtre de sync depuis > RETIRED_AFTER_DAYS. Sans ce statut, la |
|
73 |
+# ligne restait broken/stale à perpétuité (le registre est persisté, jamais |
|
74 |
+# purgé) et les agents gardiens missionnaient sans fin des sources mortes |
|
75 |
+# (2026-08-24 : 37 missions ka6 sur origin-north.ai, connecteur désactivé). |
|
76 |
+# La ligne se réévalue normalement si la source réapparaît dans le journal. |
|
77 |
+STATUS_RETIRED = "retired" |
|
78 |
+ALL_STATUSES = (STATUS_OK, STATUS_DEGRADED, STATUS_BROKEN, STATUS_STALE, STATUS_RETIRED) |
|
79 |
+ |
|
80 |
+# Retrait après N jours sans apparaître dans aucune fenêtre de sync (marge |
|
81 |
+# large au-dessus des rotations observées, ~11-30 h selon les apps). |
|
82 |
+RETIRED_AFTER_DAYS = 14 |
| 71 |
83 |
|
| 72 |
84 |
# broken après N échecs (ou 0 résultat) consécutifs. |
| 73 |
85 |
BROKEN_THRESHOLD = 3 |
| 110 |
122 |
median_found: float | None = None |
| 111 |
123 |
consecutive_failures: int = 0 |
| 112 |
124 |
checked_at: datetime.datetime | None = None |
|
125 |
+ last_seen: datetime.datetime | None = None |
| 113 |
126 |
|
| 114 |
127 |
|
| 115 |
128 |
@dataclass |
| 397 |
410 |
median_found=row.median_found, |
| 398 |
411 |
consecutive_failures=row.consecutive_failures or 0, |
| 399 |
412 |
checked_at=_ensure_utc(row.checked_at), |
|
413 |
+ last_seen=_ensure_utc(row.last_seen), |
| 400 |
414 |
) |
| 401 |
415 |
|
| 402 |
416 |
|
| 415 |
429 |
source: str, |
| 416 |
430 |
assessment: Assessment, |
| 417 |
431 |
now: float, |
|
432 |
+ seen: bool = False, |
| 418 |
433 |
) -> str: |
| 419 |
|
− """Upsert de la ligne connector_health + alerte sur transition broken/stale.""" |
|
434 |
+ """Upsert de la ligne connector_health + alerte sur transition broken/stale. |
|
435 |
+ |
|
436 |
+ ``seen`` indique que la source est apparue dans la fenêtre de sync de ce |
|
437 |
+ run : ``last_seen`` est alors horodaté — c'est lui qui pilote le retrait |
|
438 |
+ (``retired``) des sources disparues du journal depuis > RETIRED_AFTER_DAYS. |
|
439 |
+ """ |
| 420 |
440 |
row = rows.get(source) |
| 421 |
441 |
previous_status = row.status if row is not None else None |
| 422 |
442 |
|
| 425 |
445 |
row = ConnectorHealth(service=service, source=source) |
| 426 |
446 |
session.add(row) |
| 427 |
447 |
rows[source] = row |
|
448 |
+ row.last_seen = checked_at |
|
449 |
+ if seen: |
|
450 |
+ row.last_seen = checked_at |
| 428 |
451 |
row.checked_at = checked_at |
| 429 |
452 |
row.status = assessment.status |
| 430 |
453 |
row.last_success = assessment.last_success |
| 475 |
498 |
# de 2 h, fabrika en bord de rotation de ~11 h pour un seuil de 24 h). |
| 476 |
499 |
source_stale_after = app_stale_after * (rotation_factor(payload, by_source) + 1) |
| 477 |
500 |
|
|
501 |
+ # Sources désactivées déclarées par l'app elle-même (signal explicite, |
|
502 |
+ # ex. ``disabled_stores`` de fabri-ka) : retrait immédiat. |
|
503 |
+ disabled = { |
|
504 |
+ str(s) |
|
505 |
+ for s in (payload.get("disabled_stores") or payload.get("disabled_sources") or []) |
|
506 |
+ if s |
|
507 |
+ } |
|
508 |
+ |
| 478 |
509 |
with session_scope() as session: |
| 479 |
510 |
rows = _load_rows(session, service) |
| 480 |
511 |
|
| 481 |
512 |
assessment = assess_source( |
| 482 |
513 |
[app_entry], _state_from_row(rows.get(APP_SOURCE)), now, app_stale_after |
| 483 |
514 |
) |
| 484 |
|
− counts[_apply(session, rows, service, APP_SOURCE, assessment, now)] += 1 |
|
515 |
+ counts[_apply(session, rows, service, APP_SOURCE, assessment, now, seen=True)] += 1 |
| 485 |
516 |
|
| 486 |
517 |
for source, entries in by_source.items(): |
| 487 |
518 |
assessment = assess_source( |
| 488 |
519 |
entries, _state_from_row(rows.get(source)), now, source_stale_after |
| 489 |
520 |
) |
| 490 |
|
− counts[_apply(session, rows, service, source, assessment, now)] += 1 |
|
521 |
+ counts[_apply(session, rows, service, source, assessment, now, seen=True)] += 1 |
| 491 |
522 |
|
| 492 |
|
− # Sources connues mais absentes de la fenêtre courante : réévaluer la |
| 493 |
|
− # staleness (le streak et la médiane restent inchangés). |
|
523 |
+ # Sources connues mais absentes de la fenêtre courante : retrait si |
|
524 |
+ # désactivées par l'app ou disparues du journal depuis trop longtemps, |
|
525 |
+ # sinon réévaluer la staleness (streak et médiane inchangés). |
| 494 |
526 |
for source in list(rows): |
| 495 |
527 |
if source == APP_SOURCE or source in by_source: |
| 496 |
528 |
continue |
| 497 |
|
− assessment = assess_source( |
| 498 |
|
− [], _state_from_row(rows.get(source)), now, source_stale_after |
| 499 |
|
− ) |
|
529 |
+ prev = _state_from_row(rows.get(source)) |
|
530 |
+ retired_reason = None |
|
531 |
+ if source in disabled: |
|
532 |
+ retired_reason = "désactivée par l'app (liste disabled du /api/stats)" |
|
533 |
+ elif prev.last_seen is not None and ( |
|
534 |
+ now - prev.last_seen.timestamp() > RETIRED_AFTER_DAYS * 86400 |
|
535 |
+ ): |
|
536 |
+ days = (now - prev.last_seen.timestamp()) / 86400 |
|
537 |
+ retired_reason = ( |
|
538 |
+ f"absente de tout journal de sync depuis {days:.0f} j " |
|
539 |
+ f"(seuil : {RETIRED_AFTER_DAYS} j)" |
|
540 |
+ ) |
|
541 |
+ if retired_reason is not None: |
|
542 |
+ assessment = Assessment( |
|
543 |
+ status=STATUS_RETIRED, |
|
544 |
+ last_success=prev.last_success, |
|
545 |
+ found_last=prev.found_last, |
|
546 |
+ median_found=prev.median_found, |
|
547 |
+ consecutive_failures=prev.consecutive_failures, |
|
548 |
+ message=f"retirée de la supervision — {retired_reason}", |
|
549 |
+ ) |
|
550 |
+ else: |
|
551 |
+ assessment = assess_source([], prev, now, source_stale_after) |
| 500 |
552 |
counts[_apply(session, rows, service, source, assessment, now)] += 1 |
| 501 |
553 |
|
| 502 |
554 |
return { |