using ClawdDotNet.Core.Accounting;
using ClawdDotNet.Core.Config;
namespace ClawdDotNet.Core.Deploymentcenter.Watchdog;
///
/// Leitet den Instanz-Zustand für den Heartbeat ab.
///
/// Ein Heartbeat allein beweist nur, dass ein Faden läuft. Deshalb geht der
/// selbst ermittelte Gesundheitszustand als checks mit — der Server stuft einen
/// als ok gemeldeten Beat herab, sobald eine Prüfung fehlschlägt, und nennt in
/// der Antwort die betroffene. Der klassische Fall, den das abfängt: Der Takt meldet
/// brav ok, während der Aufgaben-Scanner seit einer Stunde tot ist.
///
///
/// - error — kein OpenRouter-Key konfiguriert (Agenten deaktiviert).
/// - warning — Tagesbudget der Instanz erschöpft oder Scanner steht.
/// - ok — sonst.
///
///
/// Die Metriken sind bewusst schlank und ausschließlich numerisch: keine
/// sensiblen Nutzdaten, und nur Zahlen landen im Verlauf.
///
public sealed class InstanceHealthProvider : IInstanceHealthProvider
{
private readonly string _instanceName;
private readonly bool _agentsEnabled;
private readonly InstanceBudget _budget;
private readonly IUsageRepository? _usage;
private readonly Func _agentCount;
private readonly Func _runningChats;
private readonly Func? _schedulerRunning;
private readonly Func _now;
public InstanceHealthProvider(
string instanceName,
bool agentsEnabled,
InstanceBudget budget,
IUsageRepository? usage,
Func agentCount,
Func runningChats,
Func? schedulerRunning = null,
Func? now = null)
{
_instanceName = instanceName;
_agentsEnabled = agentsEnabled;
_budget = budget;
_usage = usage;
_agentCount = agentCount;
_runningChats = runningChats;
_schedulerRunning = schedulerRunning;
_now = now ?? (() => DateTime.Now);
}
public async Task GetAsync(CancellationToken ct)
{
var metrics = new Dictionary
{
["agentCount"] = _agentCount(),
["runningChats"] = _runningChats()
};
var checks = new Dictionary
{
["agents"] = new(_agentsEnabled,
_agentsEnabled ? null : "Kein OpenRouter-API-Key konfiguriert.")
};
if (_schedulerRunning is not null)
{
var running = _schedulerRunning();
checks["scheduler"] = new(running,
running ? null : "Aufgaben-Scanner läuft nicht.");
}
string? budgetProblem = null;
if (_usage is not null)
{
var today = DateOnly.FromDateTime(_now());
var used = await _usage.GetDailyAsync(today, agentId: "", ct).ConfigureAwait(false);
metrics["todayCostUsd"] = (double)decimal.Round(used.CostUsd, 4);
metrics["todayTokens"] = used.TotalTokens;
if (Exceeds(_budget.DailyCostUsd, used.CostUsd))
{
budgetProblem =
$"Tagesbudget erschöpft: {used.CostUsd:F2} von {_budget.DailyCostUsd:F2} USD.";
}
else if (Exceeds(_budget.DailyTokens, used.TotalTokens))
{
budgetProblem =
$"Token-Tageslimit erschöpft: {used.TotalTokens:N0} von {_budget.DailyTokens:N0}.";
}
checks["budget"] = new(budgetProblem is null, budgetProblem);
}
// Der Instanzname steht in der Meldung, nicht in den Metriken: Metriken sind
// Zahlen, alles andere würde der Server beim Verdichten ohnehin verwerfen.
if (!_agentsEnabled)
{
return new InstanceHealth(
WatchdogStatus.Error,
$"{_instanceName}: Kein OpenRouter-API-Key konfiguriert – Agenten deaktiviert.",
metrics, checks);
}
if (budgetProblem is not null)
return new InstanceHealth(WatchdogStatus.Warning, $"{_instanceName}: {budgetProblem}", metrics, checks);
if (_schedulerRunning is not null && !_schedulerRunning())
{
return new InstanceHealth(
WatchdogStatus.Warning,
$"{_instanceName}: Aufgaben-Scanner läuft nicht.",
metrics, checks);
}
return new InstanceHealth(WatchdogStatus.Ok, $"{_instanceName}: Betrieb normal.", metrics, checks);
}
/// 0 oder kleiner bedeutet: keine Grenze gesetzt. Deckungsgleich mit BudgetGuard.
private static bool Exceeds(decimal limit, decimal used) => limit > 0 && used >= limit;
private static bool Exceeds(long limit, long used) => limit > 0 && used >= limit;
}