diff --git a/app/app/Services/Dashboard/BalanceHealth.php b/app/app/Services/Dashboard/BalanceHealth.php index 3994eb8a..801ba6ce 100644 --- a/app/app/Services/Dashboard/BalanceHealth.php +++ b/app/app/Services/Dashboard/BalanceHealth.php @@ -24,8 +24,9 @@ class BalanceHealth float $redFloor, float $amberFloor, ): array { + // Отрицательный/нулевой баланс → денег уже нет: 0 дней (не отрицательное «−1 дн.»). $days = ($dailySpend !== null && $dailySpend > 0) - ? (int) floor($balance / $dailySpend) + ? max(0, (int) floor($balance / $dailySpend)) : null; $light = 'green'; diff --git a/app/app/Services/External/DadataBalanceProvider.php b/app/app/Services/External/DadataBalanceProvider.php index c5468d3e..5c63369a 100644 --- a/app/app/Services/External/DadataBalanceProvider.php +++ b/app/app/Services/External/DadataBalanceProvider.php @@ -25,8 +25,15 @@ class DadataBalanceProvider implements BalanceProvider if ($key === '') { return BalanceReading::fail('dadata', 'DaData api_key не задан'); } + // Эндпоинт profile/balance требует ОБА ключа: Authorization: Token + // И X-Secret: (иначе HTTP 401). secret — тот же, что для cleaner API. + $headers = ['Authorization' => 'Token '.$key, 'Accept' => 'application/json']; + $secret = (string) config('services.dadata.secret'); + if ($secret !== '') { + $headers['X-Secret'] = $secret; + } $resp = Http::timeout(10) - ->withHeaders(['Authorization' => 'Token '.$key, 'Accept' => 'application/json']) + ->withHeaders($headers) ->get((string) config('services.dadata.balance_url')); if (! $resp->ok()) { return BalanceReading::fail('dadata', 'HTTP '.$resp->status()); diff --git a/app/tests/Unit/Dashboard/BalanceHealthTest.php b/app/tests/Unit/Dashboard/BalanceHealthTest.php index b2f67e3a..feda48f5 100644 --- a/app/tests/Unit/Dashboard/BalanceHealthTest.php +++ b/app/tests/Unit/Dashboard/BalanceHealthTest.php @@ -20,4 +20,6 @@ it('красный при низком балансе ИЛИ малом числ expect(BalanceHealth::evaluate(600, 100, 0, 0)['light'])->toBe('amber'); // отрицательный баланс (YC в минусе) → red expect(BalanceHealth::evaluate(-540, null, 1000, 5000))->toMatchArray(['days_left' => null, 'light' => 'red']); + // отрицательный баланс + известный расход → days_left КЛАМПИТСЯ к 0 (не «−1 дн.») + expect(BalanceHealth::evaluate(-591.76, 600, 1000, 5000))->toMatchArray(['days_left' => 0, 'light' => 'red']); }); diff --git a/app/tests/Unit/External/DadataBalanceProviderTest.php b/app/tests/Unit/External/DadataBalanceProviderTest.php index 6179ba0f..88611227 100644 --- a/app/tests/Unit/External/DadataBalanceProviderTest.php +++ b/app/tests/Unit/External/DadataBalanceProviderTest.php @@ -8,13 +8,16 @@ use Tests\TestCase; uses(TestCase::class); // нужен booted-app: config()/app()/Http::fake() -it('читает баланс DaData по API', function () { +it('читает баланс DaData по API c X-Secret', function () { config()->set('services.dadata.api_key', 'test-token'); + config()->set('services.dadata.secret', 'test-secret'); Http::fake(['dadata.ru/*' => Http::response(['balance' => 4500.0], 200)]); $r = app(DadataBalanceProvider::class)->fetch(); expect($r->ok)->toBeTrue(); expect($r->balance)->toBe(4500.0); expect($r->serviceKey)->toBe('dadata'); + Http::assertSent(fn ($req) => $req->hasHeader('Authorization', 'Token test-token') + && $req->hasHeader('X-Secret', 'test-secret')); }); it('ошибка API → fail, не бросает', function () { diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index a8251531..b9e5f80e 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-06-28T04:12:57.855Z +Last updated: 2026-06-28T04:13:25.256Z | Контролёр | Состояние | Детали | |---|---|---| @@ -129,9 +129,9 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 3440 | MsMpEng | 17.01ч | 0.0ч | +| 3440 | MsMpEng | 17.01ч | 220544.2ч | | 21928 | Code | 7.32ч | 0.0ч | -| 1212 | svchost | 4.38ч | 0.0ч | +| 1212 | svchost | 4.38ч | 10093734.7ч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.