fix(monitoring): MONITORING_ATTEMPTS is now exact (off-by-one); accept int or dict monitor id

- attempt is 0-based, so compare one-based: ATTEMPTS=2 really means two tries
- bridge accepts add_monitor returning {monitorID}|{id}|int (v1.2.1 returns a
  dict — re-verified: first create on a fresh Kuma is HTTP 200 with the id)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 20:08:07 +02:00
parent 5301ce4309
commit 82916167cf
4 changed files with 20 additions and 9 deletions

View File

@ -41,8 +41,10 @@ class RegisterMonitoring extends CustomerStep
throw $e; // operator wants monitoring to gate delivery throw $e; // operator wants monitoring to gate delivery
} }
$allowed = (int) config('provisioning.monitoring.attempts', 2); // `attempt` is 0-based (this call IS attempt number attempt+1), so
if ($run->attempt < $allowed) { // compare one-based: MONITORING_ATTEMPTS=2 really means two tries.
$allowed = max(1, (int) config('provisioning.monitoring.attempts', 2));
if ($run->attempt + 1 < $allowed) {
return StepResult::retry(30, 'monitoring unavailable: '.$e->getMessage()); return StepResult::retry(30, 'monitoring unavailable: '.$e->getMessage());
} }

View File

@ -163,11 +163,19 @@ def create_monitor(payload: MonitorIn) -> dict:
maxretries=payload.retries or 1, # Kuma's own field name maxretries=payload.retries or 1, # Kuma's own field name
) )
) )
monitor_id = created.get("monitorID") if isinstance(created, dict) else None # uptime-kuma-api 1.2.x returns {"msg": ..., "monitorID": N} (verified against
# Kuma 1.x). Accept a bare id too, so a future library change can't break us.
if isinstance(created, dict):
monitor_id = created.get("monitorID", created.get("id"))
elif isinstance(created, int):
monitor_id = created
else:
monitor_id = None
if monitor_id is None: if monitor_id is None:
raise HTTPException(status_code=502, detail=f"Kuma returned no monitor id: {created}") raise HTTPException(status_code=502, detail=f"Kuma returned no monitor id: {created}")
return {"monitor": {"id": monitor_id}} return {"monitor": {"id": int(monitor_id)}}
@app.get("/monitors/{monitor_id}", dependencies=[Depends(require_token)]) @app.get("/monitors/{monitor_id}", dependencies=[Depends(require_token)])

View File

@ -64,7 +64,7 @@ Alle Monitor-Routen erfordern `Authorization: Bearer $BRIDGE_TOKEN`.
Monitoring ist Beobachtung, nicht das Produkt. Fällt Kuma oder die Bridge aus, Monitoring ist Beobachtung, nicht das Produkt. Fällt Kuma oder die Bridge aus,
darf die Bereitstellung eines **bezahlten** Kunden nicht scheitern: darf die Bereitstellung eines **bezahlten** Kunden nicht scheitern:
- Der Schritt `register_monitoring` versucht es `MONITORING_ATTEMPTS` mal (Standard 2), - Der Schritt `register_monitoring` versucht es genau `MONITORING_ATTEMPTS` mal (Standard 2 = 1 Versuch + 1 Wiederholung),
- danach läuft die Bereitstellung **degradiert** weiter und schreibt ein - danach läuft die Bereitstellung **degradiert** weiter und schreibt ein
sichtbares `info`-Ereignis in die Admin-Konsole, sichtbares `info`-Ereignis in die Admin-Konsole,
- mit `MONITORING_REQUIRED=true` wird Monitoring stattdessen erzwungen (der Lauf - mit `MONITORING_REQUIRED=true` wird Monitoring stattdessen erzwungen (der Lauf

View File

@ -371,13 +371,14 @@ it('keeps provisioning when monitoring is unreachable (retry, then degrade)', fu
$s['monitoring']->failWith = 'connection refused'; $s['monitoring']->failWith = 'connection refused';
['run' => $run, 'instance' => $instance] = reservedRun(); ['run' => $run, 'instance' => $instance] = reservedRun();
// Early attempts retry — the outage may be transient. // MONITORING_ATTEMPTS=2 means exactly two tries: attempt 0 retries…
config()->set('provisioning.monitoring.attempts', 2);
$first = app(RegisterMonitoring::class)->execute($run->fresh()); $first = app(RegisterMonitoring::class)->execute($run->fresh());
expect($first->type)->toBe('retry')->and($first->reason)->toContain('connection refused'); expect($first->type)->toBe('retry')->and($first->reason)->toContain('connection refused');
// Once the allowance is used up, continue DEGRADED with a visible event // …and the SECOND try (attempt 1) is the last: continue DEGRADED with a
// instead of failing a paid customer's provisioning. // visible event instead of failing a paid customer's provisioning.
$run->update(['attempt' => (int) config('provisioning.monitoring.attempts', 2)]); $run->update(['attempt' => 1]);
expect(app(RegisterMonitoring::class)->execute($run->fresh())->type)->toBe('advance'); expect(app(RegisterMonitoring::class)->execute($run->fresh())->type)->toBe('advance');
expect($instance->fresh()->monitoringTargets()->count())->toBe(0) expect($instance->fresh()->monitoringTargets()->count())->toBe(0)