| 441 | for service, block in mine.items(): |
441 | for service, block in mine.items(): |
| 442 | if service not in SERVICES: |
442 | if service not in SERVICES: |
| 443 | continue # service inconnu de la topologie: visible au dashboard, pas d'action |
443 | continue # service inconnu de la topologie: visible au dashboard, pas d'action |
| |
444 | + to_create: list[tuple[str, str, dict[str, Any]]] = [] |
| 444 | for conn in block.get("connectors", []): |
445 | for conn in block.get("connectors", []): |
| 445 | source, status = conn["source"], conn["status"] |
446 | source, status = conn["source"], conn["status"] |
| 446 | inc = active_incident(c, service, source) |
447 | inc = active_incident(c, service, source) |
| 457 | (service, source)).fetchone() |
458 | (service, source)).fetchone() |
| 458 | if ab and now() - ab["updated"] < POLICY.get("abandoned_retry_hours", 168) * 3600: |
459 | if ab and now() - ab["updated"] < POLICY.get("abandoned_retry_hours", 168) * 3600: |
| 459 | continue |
460 | continue |
| 460 | − iid = uuid.uuid4().hex[:10] |
461 | + to_create.append((source, status, conn)) |
| 461 | − c.execute("INSERT INTO incidents(id,service,source,status_detected,state,created,updated,detail) " |
| |
| 462 | − "VALUES(?,?,?,?,?,?,?,?)", |
| |
| 463 | − (iid, service, source, status, "open", now(), now(), jdump(conn))) |
| |
| 464 | − incident_event(iid, service, source, "open", f"détecté {status}") |
| |
| 465 | elif status in trigger and inc is not None and inc["state"] in ("open", "cooldown"): |
462 | elif status in trigger and inc is not None and inc["state"] in ("open", "cooldown"): |
| 466 | # Ne pas rafraîchir watching/fixing: `updated` sert de chrono |
463 | # Ne pas rafraîchir watching/fixing: `updated` sert de chrono |
| 467 | # à la fenêtre de surveillance et au cooldown. |
464 | # à la fenêtre de surveillance et au cooldown. |
| 474 | elif inc["state"] in ("open", "cooldown"): |
471 | elif inc["state"] in ("open", "cooldown"): |
| 475 | set_incident(c, inc["id"], state="self_healed", resolved=now()) |
472 | set_incident(c, inc["id"], state="self_healed", resolved=now()) |
| 476 | incident_event(inc["id"], service, source, "self_healed", "revenu à ok sans intervention") |
473 | incident_event(inc["id"], service, source, "self_healed", "revenu à ok sans intervention") |
| |
474 | + elif status == "retired" and inc is not None and inc["state"] in ("open", "cooldown", "watching"): |
| |
475 | + # api-ka a retiré la source de la supervision (désactivée |
| |
476 | + # côté app ou disparue du journal) : plus rien à réparer. |
| |
477 | + set_incident(c, inc["id"], state="abandoned") |
| |
478 | + incident_event(inc["id"], service, source, "abandoned", "source retirée de la supervision api-ka") |
| |
479 | + |
| |
480 | + # Damper d'événement de masse : des centaines de sources qui |
| |
481 | + # basculent d'un coup signalent une panne GLOBALE de l'app (sync |
| |
482 | + # mort, /api/stats cassé), pas autant de pannes individuelles — |
| |
483 | + # missionner source par source serait long et coûteux pour rien |
| |
484 | + # (2026-08-24 : 794 incidents jobka créés après ~17 h d'arrêt de |
| |
485 | + # job-ka-sync). On n'ouvre rien : la panne d'app se voit au |
| |
486 | + # dashboard (bloc _app) et les sources encore cassées une fois la |
| |
487 | + # vague retombée sous le seuil recevront leurs incidents. |
| |
488 | + limit = POLICY.get("mass_incident_threshold", 20) |
| |
489 | + if len(to_create) > limit: |
| |
490 | + msg = (f"vague de {len(to_create)} connecteurs en panne sur {service} " |
| |
491 | + f"(seuil {limit}) — panne globale probable de l'app, aucun incident créé") |
| |
492 | + print(f"[poll] {msg}", flush=True) |
| |
493 | + hub.publish_sync({"kind": "log", "level": "error", "msg": msg, "ts": now()}) |
| |
494 | + continue |
| |
495 | + for source, status, conn in to_create: |
| |
496 | + iid = uuid.uuid4().hex[:10] |
| |
497 | + c.execute("INSERT INTO incidents(id,service,source,status_detected,state,created,updated,detail) " |
| |
498 | + "VALUES(?,?,?,?,?,?,?,?)", |
| |
499 | + (iid, service, source, status, "open", now(), now(), jdump(conn))) |
| |
500 | + incident_event(iid, service, source, "open", f"détecté {status}") |
| 477 | |
501 | |
| 478 | |
502 | |
| 479 | async def tick() -> None: |
503 | async def tick() -> None: |