iobroker.ai-usage 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/admin/i18n/de.json +13 -1
- package/admin/i18n/en.json +13 -1
- package/admin/i18n/es.json +13 -1
- package/admin/i18n/fr.json +13 -1
- package/admin/i18n/it.json +13 -1
- package/admin/i18n/nl.json +13 -1
- package/admin/i18n/pl.json +13 -1
- package/admin/i18n/pt.json +13 -1
- package/admin/i18n/ru.json +13 -1
- package/admin/i18n/uk.json +13 -1
- package/admin/i18n/zh-cn.json +13 -1
- package/build/lib/poll-engine.js +42 -5
- package/build/lib/poll-engine.js.map +2 -2
- package/build/lib/snapshot-tree.js +70 -8
- package/build/lib/snapshot-tree.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,6 +151,11 @@ so instead of pretending to be connected; signing in again is all it takes.
|
|
|
151
151
|
### **WORK IN PROGRESS**
|
|
152
152
|
-->
|
|
153
153
|
|
|
154
|
+
### 0.12.1 (2026-09-07)
|
|
155
|
+
|
|
156
|
+
- Fixed: The last-update stamp of an account no longer moves forward while the provider is only throttling — it dates the values standing next to it, so you can see how old they really are
|
|
157
|
+
- Improved: Twenty-five more datapoints explain themselves in the object tree — what "today" means (the provider counts it in UTC), and why the cost totals can be lower than the accounts show
|
|
158
|
+
|
|
154
159
|
### 0.12.0 (2026-09-06)
|
|
155
160
|
|
|
156
161
|
- Fixed: An account that has not been signed in yet no longer reports a rejected sign-in — no warning, no notification, and the settings page keeps offering the sign-in button
|
|
@@ -195,10 +200,6 @@ so instead of pretending to be connected; signing in again is all it takes.
|
|
|
195
200
|
|
|
196
201
|
- Fixed: The first start after updating no longer leaves a warning in the log
|
|
197
202
|
|
|
198
|
-
### 0.9.2 (2026-08-27)
|
|
199
|
-
|
|
200
|
-
- Fixed: Stopping the instance now marks the accounts as offline on installations that were updated too, not only on fresh ones — the previous version left them showing as online
|
|
201
|
-
|
|
202
203
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
203
204
|
|
|
204
205
|
## Support
|
package/admin/i18n/de.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Wann dieses Fenster von vorn beginnt. Leer, solange kein Fenster läuft — der Anbieter meldet dann keinen Zeitpunkt.",
|
|
68
68
|
"descResetCredits": "Käufliche Gutscheine, die ein volles Limit-Fenster zurücksetzen. Aus der Liste des Anbieters gezählt, abgelaufene bleiben draußen.",
|
|
69
69
|
"descCostsProjected": "Hochgerechnete Ausgaben zum Monatsende, aus dem bisher Ausgegebenen berechnet — keine Zahl des Anbieters.",
|
|
70
|
-
"descTotalMaxPercent": "Das vollste Konto: sein plan-weites Fenster oder sein Restguthaben, je nachdem, was höher liegt."
|
|
70
|
+
"descTotalMaxPercent": "Das vollste Konto: sein plan-weites Fenster oder sein Restguthaben, je nachdem, was höher liegt.",
|
|
71
|
+
"descLastUpdate": "Wann dieses Konto zuletzt Daten geliefert hat. Eine gedrosselte oder abgelehnte Abfrage bewegt ihn nicht — das Alter dieses Stempels ist das Alter der Werte daneben.",
|
|
72
|
+
"descCostsToday": "Was die heutige Nutzung gekostet hat, aus dem Tagesbericht des Anbieters. Der Tag zählt in UTC — der Zähler beginnt vor der lokalen Mitternacht von vorn.",
|
|
73
|
+
"descCostsMonth": "Was die Nutzung selbst diesen Monat gekostet hat. Der Preis eines Abos steckt nie darin — nur, was zusätzlich zum Tarif berechnet wird.",
|
|
74
|
+
"descTokensToday": "Aus dem Tages-Nutzungsbericht des Anbieters gezählt. Der Tag zählt in UTC — der Zähler beginnt vor der lokalen Mitternacht von vorn.",
|
|
75
|
+
"descModelTokens": "Eingabe- und Ausgabe-Token dieses Modells zusammengezählt — die Zähler unter „Tokens“ halten beide auseinander.",
|
|
76
|
+
"descCreditsGranted": "Der Teil des Guthabens, den der Anbieter geschenkt hat. „Verbleibendes Guthaben“ ist der ganze Stand; das Eingezahlte steht unter „Aufgeladenes Guthaben“.",
|
|
77
|
+
"descCreditsToppedUp": "Der Teil des Guthabens, der eingezahlt wurde. „Verbleibendes Guthaben“ ist der ganze Stand; das Geschenkte steht unter „Gewährtes Guthaben“.",
|
|
78
|
+
"descResetCreditsExpiry": "Wann der zuerst ablaufende Gutschein verfällt. Leer, solange kein Gutschein vorhanden ist — der Datenpunkt bleibt, nur sein Wert leert sich.",
|
|
79
|
+
"descTotalCosts": "Nur über die Konten summiert, die in USD abrechnen. Andere Währungen und Stückzähler (Anfrage-Guthaben, Reset-Gutscheine) bleiben draußen — der Wert kann also unter dem der Konten liegen.",
|
|
80
|
+
"descTotalCostsProjected": "Hochgerechnete Ausgaben zum Monatsende, aus dem bisher Ausgegebenen berechnet — keine Zahl des Anbieters. Nur über die Konten summiert, die in USD abrechnen.",
|
|
81
|
+
"descTotalAccounts": "Jedes in den Einstellungen eingeschaltete Konto — auch die, die der Adapter mangels Zugangsdaten gar nicht abfragen kann.",
|
|
82
|
+
"descTotalReachable": "Wie viele Konten als liefernd gelten. Ein gedrosseltes Konto bleibt mitgezählt — es behält seine letzten Werte, und seine letzte erfolgreiche Aktualisierung sagt, wie alt die sind."
|
|
71
83
|
}
|
package/admin/i18n/en.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "When this window starts over. Empty while no window is running — the provider reports no reset time then.",
|
|
68
68
|
"descResetCredits": "Purchasable vouchers that clear a full limit window. Counted from the provider's list, expired ones left out.",
|
|
69
69
|
"descCostsProjected": "Projected spend at month end, computed from what was spent so far — not a figure the provider reports.",
|
|
70
|
-
"descTotalMaxPercent": "The fullest account: its plan-wide window or its remaining budget, whichever is higher."
|
|
70
|
+
"descTotalMaxPercent": "The fullest account: its plan-wide window or its remaining budget, whichever is higher.",
|
|
71
|
+
"descLastUpdate": "When this account last delivered data. A throttled or rejected query does not move it, so the age of this stamp is the age of the values beside it.",
|
|
72
|
+
"descCostsToday": "Money billed for today's use, from the provider's daily report. The day is counted in UTC, so the counter starts over before local midnight.",
|
|
73
|
+
"descCostsMonth": "What the use itself has cost this month. The price of a subscription is never part of it — only what is billed on top of the plan.",
|
|
74
|
+
"descTokensToday": "Counted from the provider's own daily usage report. The day is counted in UTC, so the counter starts over before local midnight.",
|
|
75
|
+
"descModelTokens": "Input and output tokens of this model added together — the counters under Tokens keep the two apart.",
|
|
76
|
+
"descCreditsGranted": "The part of the balance the provider gave for free. Credits remaining is the whole balance; what was paid in stands under Topped-up balance.",
|
|
77
|
+
"descCreditsToppedUp": "The part of the balance that was paid in. Credits remaining is the whole balance; what the provider gave for free stands under Granted balance.",
|
|
78
|
+
"descResetCreditsExpiry": "When the voucher that runs out first expires. Empty while no voucher is held — the datapoint stays, only its value empties.",
|
|
79
|
+
"descTotalCosts": "Summed over the accounts that bill in USD only. Other currencies and piece counters (request credits, reset vouchers) stay out, so this can be less than the accounts show.",
|
|
80
|
+
"descTotalCostsProjected": "Projected spend at month end, computed from what was spent so far — not a figure the provider reports. Summed over the accounts that bill in USD only.",
|
|
81
|
+
"descTotalAccounts": "Every account switched on in the settings, including the ones the adapter cannot query because their credential is missing.",
|
|
82
|
+
"descTotalReachable": "How many accounts count as delivering. A throttled account stays in the count — it keeps its last values, and its last successful update says how old they are."
|
|
71
83
|
}
|
package/admin/i18n/es.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Cuándo vuelve a empezar esta ventana. Vacío mientras no hay ventana en curso — el proveedor no indica entonces una hora.",
|
|
68
68
|
"descResetCredits": "Vales comprables que reinician una ventana de límite llena. Contados desde la lista del proveedor, los caducados quedan fuera.",
|
|
69
69
|
"descCostsProjected": "Gasto proyectado a fin de mes, calculado a partir de lo ya gastado — no es una cifra del proveedor.",
|
|
70
|
-
"descTotalMaxPercent": "La cuenta más llena: su ventana del plan o su presupuesto restante, lo que sea mayor."
|
|
70
|
+
"descTotalMaxPercent": "La cuenta más llena: su ventana del plan o su presupuesto restante, lo que sea mayor.",
|
|
71
|
+
"descLastUpdate": "Cuándo entregó datos esta cuenta por última vez. Una consulta limitada o rechazada no lo mueve, así que la antigüedad de esta marca es la de los valores contiguos.",
|
|
72
|
+
"descCostsToday": "El importe facturado por el uso de hoy, del informe diario del proveedor. El día se cuenta en UTC, así que el contador se reinicia antes de la medianoche local.",
|
|
73
|
+
"descCostsMonth": "Lo que ha costado el uso en sí este mes. El precio de una suscripción nunca forma parte de ello — solo lo que se factura además del plan.",
|
|
74
|
+
"descTokensToday": "Contado desde el informe diario de uso del propio proveedor. El día se cuenta en UTC, así que el contador se reinicia antes de la medianoche local.",
|
|
75
|
+
"descModelTokens": "Los tokens de entrada y de salida de este modelo sumados — los contadores bajo «Tokens» los mantienen separados.",
|
|
76
|
+
"descCreditsGranted": "La parte del saldo que el proveedor regaló. «Créditos restantes» es el saldo completo; lo ingresado figura en «Saldo recargado».",
|
|
77
|
+
"descCreditsToppedUp": "La parte del saldo que se ingresó. «Créditos restantes» es el saldo completo; lo que el proveedor regaló figura en «Saldo concedido».",
|
|
78
|
+
"descResetCreditsExpiry": "Cuándo caduca el vale que expira primero. Vacío mientras no se posee ningún vale — el dato permanece, solo se vacía su valor.",
|
|
79
|
+
"descTotalCosts": "Sumado solo sobre las cuentas que facturan en USD. Otras monedas y contadores por unidades (créditos de solicitudes, vales de reinicio) quedan fuera, así que puede ser menor que lo que muestran las cuentas.",
|
|
80
|
+
"descTotalCostsProjected": "Gasto proyectado a fin de mes, calculado a partir de lo ya gastado — no es una cifra del proveedor. Sumado solo sobre las cuentas que facturan en USD.",
|
|
81
|
+
"descTotalAccounts": "Todas las cuentas activadas en la configuración, incluidas aquellas que el adaptador no puede consultar por faltarle sus credenciales.",
|
|
82
|
+
"descTotalReachable": "Cuántas cuentas cuentan como entregando datos. Una cuenta limitada sigue contando — conserva sus últimos valores, y su última actualización correcta dice cuán antiguos son."
|
|
71
83
|
}
|
package/admin/i18n/fr.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Quand cette fenêtre recommence. Vide tant qu'aucune fenêtre n'est en cours — le fournisseur n'indique alors aucune heure.",
|
|
68
68
|
"descResetCredits": "Bons achetables qui réinitialisent une fenêtre de limite pleine. Comptés depuis la liste du fournisseur, les expirés exclus.",
|
|
69
69
|
"descCostsProjected": "Dépense projetée en fin de mois, calculée à partir du montant déjà dépensé — ce n'est pas un chiffre du fournisseur.",
|
|
70
|
-
"descTotalMaxPercent": "Le compte le plus rempli : sa fenêtre de forfait ou son budget restant, selon ce qui est le plus élevé."
|
|
70
|
+
"descTotalMaxPercent": "Le compte le plus rempli : sa fenêtre de forfait ou son budget restant, selon ce qui est le plus élevé.",
|
|
71
|
+
"descLastUpdate": "Quand ce compte a fourni des données pour la dernière fois. Une requête limitée ou refusée ne la fait pas avancer : l'âge de cet horodatage est celui des valeurs voisines.",
|
|
72
|
+
"descCostsToday": "Le montant facturé pour l'usage du jour, d'après le rapport quotidien du fournisseur. Le jour est compté en UTC : le compteur repart avant minuit local.",
|
|
73
|
+
"descCostsMonth": "Ce que l'usage lui-même a coûté ce mois-ci. Le prix d'un abonnement n'y figure jamais — uniquement ce qui est facturé en plus du forfait.",
|
|
74
|
+
"descTokensToday": "Compté à partir du rapport d'usage quotidien du fournisseur. Le jour est compté en UTC : le compteur repart avant minuit local.",
|
|
75
|
+
"descModelTokens": "Les jetons d'entrée et de sortie de ce modèle additionnés — les compteurs sous « Jetons » les gardent séparés.",
|
|
76
|
+
"descCreditsGranted": "La part du solde offerte par le fournisseur. « Crédits restants » est le solde entier ; ce qui a été versé figure sous « Solde rechargé ».",
|
|
77
|
+
"descCreditsToppedUp": "La part du solde qui a été versée. « Crédits restants » est le solde entier ; ce que le fournisseur a offert figure sous « Solde offert ».",
|
|
78
|
+
"descResetCreditsExpiry": "Quand expire le bon qui arrive à échéance en premier. Vide tant qu'aucun bon n'est détenu — le point de donnée reste, seule sa valeur se vide.",
|
|
79
|
+
"descTotalCosts": "Additionné uniquement sur les comptes facturés en USD. Les autres devises et les compteurs à l'unité (crédits de requêtes, bons de réinitialisation) restent dehors : le total peut donc être inférieur à ce qu'affichent les comptes.",
|
|
80
|
+
"descTotalCostsProjected": "Dépense projetée en fin de mois, calculée à partir du montant déjà dépensé — ce n'est pas un chiffre du fournisseur. Additionné uniquement sur les comptes facturés en USD.",
|
|
81
|
+
"descTotalAccounts": "Tous les comptes activés dans les réglages, y compris ceux que l'adaptateur ne peut pas interroger faute d'identifiants.",
|
|
82
|
+
"descTotalReachable": "Combien de comptes sont considérés comme fournissant des données. Un compte limité reste compté : il conserve ses dernières valeurs, et sa dernière mise à jour réussie en donne l'âge."
|
|
71
83
|
}
|
package/admin/i18n/it.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Quando questa finestra riparte. Vuoto finché nessuna finestra è in corso — il provider non indica allora un orario.",
|
|
68
68
|
"descResetCredits": "Buoni acquistabili che azzerano una finestra di limite piena. Contati dall'elenco del provider, quelli scaduti esclusi.",
|
|
69
69
|
"descCostsProjected": "Spesa prevista a fine mese, calcolata da quanto speso finora — non è un dato del provider.",
|
|
70
|
-
"descTotalMaxPercent": "L'account più pieno: la sua finestra di piano o il budget residuo, a seconda di quale sia maggiore."
|
|
70
|
+
"descTotalMaxPercent": "L'account più pieno: la sua finestra di piano o il budget residuo, a seconda di quale sia maggiore.",
|
|
71
|
+
"descLastUpdate": "Quando questo account ha fornito dati l'ultima volta. Una richiesta limitata o rifiutata non lo sposta, quindi l'età di questo timestamp è quella dei valori accanto.",
|
|
72
|
+
"descCostsToday": "L'importo fatturato per l'uso di oggi, dal rapporto giornaliero del provider. Il giorno è contato in UTC, quindi il contatore riparte prima della mezzanotte locale.",
|
|
73
|
+
"descCostsMonth": "Quanto è costato l'uso stesso in questo mese. Il prezzo di un abbonamento non ne fa mai parte — solo ciò che viene fatturato oltre il piano.",
|
|
74
|
+
"descTokensToday": "Contato dal rapporto giornaliero di utilizzo del provider. Il giorno è contato in UTC, quindi il contatore riparte prima della mezzanotte locale.",
|
|
75
|
+
"descModelTokens": "I token di input e di output di questo modello sommati — i contatori sotto «Token» li tengono separati.",
|
|
76
|
+
"descCreditsGranted": "La parte del saldo regalata dal provider. «Crediti rimanenti» è il saldo intero; quanto è stato versato sta sotto «Saldo ricaricato».",
|
|
77
|
+
"descCreditsToppedUp": "La parte del saldo che è stata versata. «Crediti rimanenti» è il saldo intero; quanto il provider ha regalato sta sotto «Saldo concesso».",
|
|
78
|
+
"descResetCreditsExpiry": "Quando scade il buono che termina per primo. Vuoto finché non si possiede alcun buono — il datapoint resta, si svuota solo il suo valore.",
|
|
79
|
+
"descTotalCosts": "Sommato solo sugli account che fatturano in USD. Altre valute e contatori a pezzi (crediti di richieste, buoni di azzeramento) restano fuori, quindi il valore può essere inferiore a quello degli account.",
|
|
80
|
+
"descTotalCostsProjected": "Spesa prevista a fine mese, calcolata da quanto speso finora — non è un dato del provider. Sommato solo sugli account che fatturano in USD.",
|
|
81
|
+
"descTotalAccounts": "Ogni account attivato nelle impostazioni, comprese quelle che l'adapter non può interrogare perché mancano le credenziali.",
|
|
82
|
+
"descTotalReachable": "Quanti account risultano attivi nel fornire dati. Un account limitato resta nel conteggio — conserva gli ultimi valori, e il suo ultimo aggiornamento riuscito ne dice l'età."
|
|
71
83
|
}
|
package/admin/i18n/nl.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Wanneer dit venster opnieuw begint. Leeg zolang er geen venster loopt — de aanbieder meldt dan geen tijdstip.",
|
|
68
68
|
"descResetCredits": "Koopbare vouchers die een vol limietvenster resetten. Geteld uit de lijst van de aanbieder, verlopen exemplaren tellen niet mee.",
|
|
69
69
|
"descCostsProjected": "Verwachte uitgaven aan het einde van de maand, berekend uit wat tot nu toe is uitgegeven — geen cijfer van de aanbieder.",
|
|
70
|
-
"descTotalMaxPercent": "Het volste account: zijn planbrede venster of zijn resterende budget, wat hoger is."
|
|
70
|
+
"descTotalMaxPercent": "Het volste account: zijn planbrede venster of zijn resterende budget, wat hoger is.",
|
|
71
|
+
"descLastUpdate": "Wanneer dit account voor het laatst gegevens leverde. Een beperkte of geweigerde opvraging verzet hem niet, dus de leeftijd van dit stempel is die van de waarden ernaast.",
|
|
72
|
+
"descCostsToday": "Het bedrag dat voor het gebruik van vandaag in rekening is gebracht, uit het dagrapport van de aanbieder. De dag telt in UTC, dus de teller begint voor lokale middernacht opnieuw.",
|
|
73
|
+
"descCostsMonth": "Wat het gebruik zelf deze maand heeft gekost. De prijs van een abonnement zit er nooit in — alleen wat bovenop het plan wordt gefactureerd.",
|
|
74
|
+
"descTokensToday": "Geteld uit het dagelijkse gebruiksrapport van de aanbieder zelf. De dag telt in UTC, dus de teller begint voor lokale middernacht opnieuw.",
|
|
75
|
+
"descModelTokens": "De invoer- en uitvoertokens van dit model bij elkaar opgeteld — de tellers onder 'Tokens' houden ze uit elkaar.",
|
|
76
|
+
"descCreditsGranted": "Het deel van het tegoed dat de aanbieder gratis gaf. 'Resterend tegoed' is het hele saldo; het gestorte deel staat onder 'Opgewaardeerd tegoed'.",
|
|
77
|
+
"descCreditsToppedUp": "Het deel van het tegoed dat is gestort. 'Resterend tegoed' is het hele saldo; wat de aanbieder gratis gaf staat onder 'Toegekend tegoed'.",
|
|
78
|
+
"descResetCreditsExpiry": "Wanneer de bon die het eerst afloopt verstrijkt. Leeg zolang er geen bon is — het datapunt blijft, alleen de waarde wordt leeg.",
|
|
79
|
+
"descTotalCosts": "Alleen opgeteld over de accounts die in USD afrekenen. Andere valuta's en stuktellers (verzoektegoeden, resetbonnen) blijven erbuiten, dus dit kan lager zijn dan de accounts tonen.",
|
|
80
|
+
"descTotalCostsProjected": "Verwachte uitgaven aan het einde van de maand, berekend uit wat tot nu toe is uitgegeven — geen cijfer van de aanbieder. Alleen opgeteld over de accounts die in USD afrekenen.",
|
|
81
|
+
"descTotalAccounts": "Elk account dat in de instellingen is ingeschakeld, ook die welke de adapter niet kan opvragen omdat de inloggegevens ontbreken.",
|
|
82
|
+
"descTotalReachable": "Hoeveel accounts als leverend gelden. Een beperkt account blijft meetellen — het behoudt zijn laatste waarden, en de laatste geslaagde update zegt hoe oud die zijn."
|
|
71
83
|
}
|
package/admin/i18n/pl.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Kiedy to okno zaczyna się od nowa. Puste, dopóki żadne okno nie trwa — dostawca nie podaje wtedy godziny.",
|
|
68
68
|
"descResetCredits": "Kupowane bony resetujące pełne okno limitu. Liczone z listy dostawcy, wygasłe pomijane.",
|
|
69
69
|
"descCostsProjected": "Prognozowane wydatki na koniec miesiąca, wyliczone z dotychczasowych — to nie jest liczba od dostawcy.",
|
|
70
|
-
"descTotalMaxPercent": "Najbardziej zapełnione konto: jego okno planu albo pozostały budżet — co jest wyższe."
|
|
70
|
+
"descTotalMaxPercent": "Najbardziej zapełnione konto: jego okno planu albo pozostały budżet — co jest wyższe.",
|
|
71
|
+
"descLastUpdate": "Kiedy to konto ostatnio dostarczyło dane. Zapytanie ograniczone lub odrzucone nie przesuwa znacznika, więc jego wiek jest wiekiem sąsiednich wartości.",
|
|
72
|
+
"descCostsToday": "Kwota naliczona za dzisiejsze użycie, z dziennego raportu dostawcy. Dzień liczony jest w UTC, więc licznik zeruje się przed lokalną północą.",
|
|
73
|
+
"descCostsMonth": "Ile kosztowało samo użycie w tym miesiącu. Cena subskrypcji nigdy nie wchodzi w skład tej kwoty — tylko to, co naliczono ponad plan.",
|
|
74
|
+
"descTokensToday": "Liczone z dziennego raportu użycia samego dostawcy. Dzień liczony jest w UTC, więc licznik zeruje się przed lokalną północą.",
|
|
75
|
+
"descModelTokens": "Tokeny wejściowe i wyjściowe tego modelu zsumowane — liczniki w sekcji „Tokeny” trzymają je osobno.",
|
|
76
|
+
"descCreditsGranted": "Część środków, którą dostawca przyznał za darmo. „Pozostałe środki” to całe saldo; wpłacona część figuruje jako „Doładowane środki”.",
|
|
77
|
+
"descCreditsToppedUp": "Część środków, która została wpłacona. „Pozostałe środki” to całe saldo; to, co dostawca przyznał za darmo, figuruje jako „Przyznane środki”.",
|
|
78
|
+
"descResetCreditsExpiry": "Kiedy wygasa kupon kończący się jako pierwszy. Puste, dopóki nie ma żadnego kuponu — punkt danych pozostaje, pustoszeje tylko jego wartość.",
|
|
79
|
+
"descTotalCosts": "Sumowane tylko po kontach rozliczanych w USD. Inne waluty i liczniki sztukowe (środki na zapytania, kupony resetu) pozostają poza sumą, więc wartość może być niższa niż na kontach.",
|
|
80
|
+
"descTotalCostsProjected": "Prognozowane wydatki na koniec miesiąca, wyliczone z dotychczasowych — to nie jest liczba od dostawcy. Sumowane tylko po kontach rozliczanych w USD.",
|
|
81
|
+
"descTotalAccounts": "Każde konto włączone w ustawieniach, łącznie z tymi, których adapter nie może odpytać z braku poświadczeń.",
|
|
82
|
+
"descTotalReachable": "Ile kont uchodzi za dostarczające dane. Konto ograniczone nadal się liczy — zachowuje ostatnie wartości, a jego ostatnia udana aktualizacja mówi, jak stare one są."
|
|
71
83
|
}
|
package/admin/i18n/pt.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Quando esta janela recomeça. Vazio enquanto nenhuma janela decorre — o fornecedor não indica então um momento.",
|
|
68
68
|
"descResetCredits": "Vales compráveis que repõem uma janela de limite cheia. Contados a partir da lista do fornecedor, os expirados ficam de fora.",
|
|
69
69
|
"descCostsProjected": "Gasto projetado para o fim do mês, calculado a partir do já gasto — não é um valor do fornecedor.",
|
|
70
|
-
"descTotalMaxPercent": "A conta mais cheia: a sua janela do plano ou o orçamento restante, o que for maior."
|
|
70
|
+
"descTotalMaxPercent": "A conta mais cheia: a sua janela do plano ou o orçamento restante, o que for maior.",
|
|
71
|
+
"descLastUpdate": "Quando esta conta forneceu dados pela última vez. Uma consulta limitada ou recusada não o avança, por isso a idade desta marca é a dos valores ao lado.",
|
|
72
|
+
"descCostsToday": "O valor faturado pelo uso de hoje, do relatório diário do fornecedor. O dia é contado em UTC, pelo que o contador recomeça antes da meia-noite local.",
|
|
73
|
+
"descCostsMonth": "Quanto custou o uso em si este mês. O preço de uma subscrição nunca faz parte — apenas o que é faturado para além do plano.",
|
|
74
|
+
"descTokensToday": "Contado a partir do relatório diário de uso do próprio fornecedor. O dia é contado em UTC, pelo que o contador recomeça antes da meia-noite local.",
|
|
75
|
+
"descModelTokens": "Os tokens de entrada e de saída deste modelo somados — os contadores em «Tokens» mantêm-nos separados.",
|
|
76
|
+
"descCreditsGranted": "A parte do saldo que o fornecedor ofereceu. «Créditos restantes» é o saldo completo; o que foi carregado está em «Saldo carregado».",
|
|
77
|
+
"descCreditsToppedUp": "A parte do saldo que foi carregada. «Créditos restantes» é o saldo completo; o que o fornecedor ofereceu está em «Saldo concedido».",
|
|
78
|
+
"descResetCreditsExpiry": "Quando expira o vale que termina primeiro. Vazio enquanto não há nenhum vale — o ponto de dados permanece, apenas o seu valor fica vazio.",
|
|
79
|
+
"descTotalCosts": "Somado apenas sobre as contas que faturam em USD. Outras moedas e contadores por unidade (créditos de pedidos, vales de reposição) ficam de fora, pelo que o valor pode ser inferior ao das contas.",
|
|
80
|
+
"descTotalCostsProjected": "Gasto projetado para o fim do mês, calculado a partir do já gasto — não é um valor do fornecedor. Somado apenas sobre as contas que faturam em USD.",
|
|
81
|
+
"descTotalAccounts": "Todas as contas ativadas nas definições, incluindo aquelas que o adaptador não consegue consultar por faltarem as credenciais.",
|
|
82
|
+
"descTotalReachable": "Quantas contas contam como estando a fornecer dados. Uma conta limitada continua contada — mantém os últimos valores, e a sua última atualização bem-sucedida diz a idade deles."
|
|
71
83
|
}
|
package/admin/i18n/ru.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Когда это окно начнётся заново. Пусто, пока окно не идёт — провайдер тогда не сообщает времени сброса.",
|
|
68
68
|
"descResetCredits": "Покупаемые ваучеры, сбрасывающие полное окно лимита. Подсчитаны по списку провайдера, просроченные исключены.",
|
|
69
69
|
"descCostsProjected": "Прогноз расходов на конец месяца, рассчитанный из уже потраченного — это не данные провайдера.",
|
|
70
|
-
"descTotalMaxPercent": "Самый заполненный аккаунт: его общее окно тарифа или остаток бюджета — что выше."
|
|
70
|
+
"descTotalMaxPercent": "Самый заполненный аккаунт: его общее окно тарифа или остаток бюджета — что выше.",
|
|
71
|
+
"descLastUpdate": "Когда этот аккаунт в последний раз передал данные. Троттлинг или отказ метку не двигают, поэтому её возраст — это возраст соседних значений.",
|
|
72
|
+
"descCostsToday": "Сумма, начисленная за сегодняшнее использование, из дневного отчёта провайдера. День считается в UTC, поэтому счётчик обнуляется до местной полуночи.",
|
|
73
|
+
"descCostsMonth": "Во сколько обошлось само использование в этом месяце. Цена подписки сюда никогда не входит — только то, что начислено сверх тарифа.",
|
|
74
|
+
"descTokensToday": "Подсчитано по собственному дневному отчёту провайдера. День считается в UTC, поэтому счётчик обнуляется до местной полуночи.",
|
|
75
|
+
"descModelTokens": "Входные и выходные токены этой модели вместе — счётчики в разделе «Токены» держат их раздельно.",
|
|
76
|
+
"descCreditsGranted": "Часть баланса, которую провайдер начислил бесплатно. «Остаток кредитов» — это весь баланс; внесённая часть указана в «Пополненном балансе».",
|
|
77
|
+
"descCreditsToppedUp": "Часть баланса, которая была внесена. «Остаток кредитов» — это весь баланс; начисленное бесплатно указано в «Предоставленном балансе».",
|
|
78
|
+
"descResetCreditsExpiry": "Когда истекает ваучер, срок которого заканчивается первым. Пусто, пока ваучеров нет — точка данных остаётся, пустеет лишь её значение.",
|
|
79
|
+
"descTotalCosts": "Суммируется только по аккаунтам, которые тарифицируются в USD. Другие валюты и штучные счётчики (кредиты запросов, ваучеры сброса) не входят, поэтому значение может быть меньше, чем у аккаунтов.",
|
|
80
|
+
"descTotalCostsProjected": "Прогноз расходов на конец месяца, рассчитанный из уже потраченного — это не данные провайдера. Суммируется только по аккаунтам, которые тарифицируются в USD.",
|
|
81
|
+
"descTotalAccounts": "Каждый включённый в настройках аккаунт — в том числе те, которые адаптер не может опросить из-за отсутствия учётных данных.",
|
|
82
|
+
"descTotalReachable": "Сколько аккаунтов считаются передающими данные. Аккаунт под троттлингом остаётся в счёте — он сохраняет последние значения, а его последнее успешное обновление говорит, насколько они стары."
|
|
71
83
|
}
|
package/admin/i18n/uk.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "Коли це вікно почнеться заново. Порожньо, поки вікно не триває — провайдер тоді не повідомляє час.",
|
|
68
68
|
"descResetCredits": "Ваучери, які можна купити, щоб скинути повне вікно ліміту. Пораховані зі списку провайдера, прострочені не враховано.",
|
|
69
69
|
"descCostsProjected": "Прогноз витрат на кінець місяця, розрахований із уже витраченого — це не число від провайдера.",
|
|
70
|
-
"descTotalMaxPercent": "Найзаповненіший акаунт: його вікно тарифу або залишок бюджету — що більше."
|
|
70
|
+
"descTotalMaxPercent": "Найзаповненіший акаунт: його вікно тарифу або залишок бюджету — що більше.",
|
|
71
|
+
"descLastUpdate": "Коли цей акаунт востаннє надав дані. Обмежений або відхилений запит не зсуває позначку, тож її вік — це вік сусідніх значень.",
|
|
72
|
+
"descCostsToday": "Сума, нарахована за сьогоднішнє використання, зі щоденного звіту провайдера. День рахується в UTC, тож лічильник обнуляється до місцевої півночі.",
|
|
73
|
+
"descCostsMonth": "У скільки обійшлося саме використання цього місяця. Ціна підписки сюди ніколи не входить — лише те, що нараховано понад тариф.",
|
|
74
|
+
"descTokensToday": "Пораховано за власним щоденним звітом провайдера. День рахується в UTC, тож лічильник обнуляється до місцевої півночі.",
|
|
75
|
+
"descModelTokens": "Вхідні та вихідні токени цієї моделі разом — лічильники в розділі «Токени» тримають їх окремо.",
|
|
76
|
+
"descCreditsGranted": "Частина балансу, яку провайдер нарахував безкоштовно. «Залишок кредитів» — це весь баланс; внесена частина вказана в «Поповненому балансі».",
|
|
77
|
+
"descCreditsToppedUp": "Частина балансу, яку було внесено. «Залишок кредитів» — це весь баланс; нараховане безкоштовно вказано в «Наданому балансі».",
|
|
78
|
+
"descResetCreditsExpiry": "Коли спливає ваучер, термін якого закінчується першим. Порожньо, поки ваучерів немає — точка даних залишається, порожніє лише її значення.",
|
|
79
|
+
"descTotalCosts": "Сумується лише за акаунтами, які тарифікуються в USD. Інші валюти та штучні лічильники (кредити запитів, ваучери скидання) не входять, тож значення може бути меншим, ніж в акаунтах.",
|
|
80
|
+
"descTotalCostsProjected": "Прогноз витрат на кінець місяця, розрахований із уже витраченого — це не число від провайдера. Сумується лише за акаунтами, які тарифікуються в USD.",
|
|
81
|
+
"descTotalAccounts": "Кожен увімкнений у налаштуваннях акаунт — зокрема ті, які адаптер не може опитати через відсутність облікових даних.",
|
|
82
|
+
"descTotalReachable": "Скільки акаунтів вважаються такими, що надають дані. Обмежений акаунт залишається в підрахунку — він зберігає останні значення, а його останнє успішне оновлення каже, наскільки вони старі."
|
|
71
83
|
}
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -67,5 +67,17 @@
|
|
|
67
67
|
"descWindowResetAt": "此窗口何时重新开始。没有窗口在运行时为空——供应商此时不提供重置时间。",
|
|
68
68
|
"descResetCredits": "可购买的额度券,用于重置已用满的限额窗口。按供应商列表统计,已过期的不计入。",
|
|
69
69
|
"descCostsProjected": "按目前支出推算的月末花费——不是供应商给出的数字。",
|
|
70
|
-
"descTotalMaxPercent": "最满的账户:其套餐窗口或剩余额度,取较高者。"
|
|
70
|
+
"descTotalMaxPercent": "最满的账户:其套餐窗口或剩余额度,取较高者。",
|
|
71
|
+
"descLastUpdate": "此账户最近一次提供数据的时间。被限流或被拒绝的查询不会推进它,因此该时间戳的新旧即为旁边数值的新旧。",
|
|
72
|
+
"descCostsToday": "供应商日报表中今日用量的计费金额。日期按 UTC 计算,因此计数会在本地午夜之前归零。",
|
|
73
|
+
"descCostsMonth": "本月使用本身产生的费用。订阅费从不计入其中,只包含套餐之外另行计费的部分。",
|
|
74
|
+
"descTokensToday": "取自供应商自己的每日用量报表。日期按 UTC 计算,因此计数会在本地午夜之前归零。",
|
|
75
|
+
"descModelTokens": "该模型的输入与输出 Token 之和——“Token”下的计数器则将两者分开统计。",
|
|
76
|
+
"descCreditsGranted": "余额中由供应商赠送的部分。“剩余额度”是全部余额;自行充值的部分见“充值余额”。",
|
|
77
|
+
"descCreditsToppedUp": "余额中自行充值的部分。“剩余额度”是全部余额;由供应商赠送的部分见“赠送余额”。",
|
|
78
|
+
"descResetCreditsExpiry": "最先到期的额度券的到期时间。没有额度券时为空——数据点保留,仅数值清空。",
|
|
79
|
+
"descTotalCosts": "仅对以美元计费的账户求和。其他币种与按次计数(请求额度、重置券)不计入,因此该值可能低于各账户所显示的数额。",
|
|
80
|
+
"descTotalCostsProjected": "按目前支出推算的月末花费——不是供应商给出的数字。仅对以美元计费的账户求和。",
|
|
81
|
+
"descTotalAccounts": "在设置中启用的所有账户,包括因缺少凭据而无法查询的账户。",
|
|
82
|
+
"descTotalReachable": "有多少账户被视为正在提供数据。被限流的账户仍计入其中——它保留最后的数值,其“最近一次成功更新”说明这些数值有多旧。"
|
|
71
83
|
}
|
package/build/lib/poll-engine.js
CHANGED
|
@@ -396,12 +396,19 @@ class PollEngine {
|
|
|
396
396
|
/**
|
|
397
397
|
* Write one account's info states (offline marker, error text, last update).
|
|
398
398
|
*
|
|
399
|
+
* The stamp only moves when a poll actually brought a snapshot. It used to hang
|
|
400
|
+
* on `reachable`, and that includes `rate-limited` by design — so every throttled
|
|
401
|
+
* poll re-dated values it had not fetched: after a day of throttling the stamp
|
|
402
|
+
* read "an hour ago" next to day-old numbers, which is the one thing a datapoint
|
|
403
|
+
* called "last successful update" must never do (measured 2026-09-07 while
|
|
404
|
+
* writing its description).
|
|
405
|
+
*
|
|
399
406
|
* @param runtime the account's runtime
|
|
400
407
|
*/
|
|
401
408
|
writeAccountInfo(runtime) {
|
|
402
409
|
const { config } = runtime;
|
|
403
410
|
void this.writeAccountStatus(runtime);
|
|
404
|
-
if (runtime.
|
|
411
|
+
if (runtime.state === "ok") {
|
|
405
412
|
this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());
|
|
406
413
|
}
|
|
407
414
|
}
|
|
@@ -456,6 +463,13 @@ class PollEngine {
|
|
|
456
463
|
common: {
|
|
457
464
|
// The readable provider name, never the internal kind: the node used to
|
|
458
465
|
// read "Claude (claude-sub)" and "My key (anthropic-api)" in the tree.
|
|
466
|
+
//
|
|
467
|
+
// The bracket stays even when it repeats the name ("Claude (Claude)") —
|
|
468
|
+
// krobi decided that on 2026-09-06 after seeing that the repetition is
|
|
469
|
+
// the normal case (the settings page names a subscription row after its
|
|
470
|
+
// provider, and a key account inherits the name of its credential entry).
|
|
471
|
+
// The provider is then always visible, including for a freely named
|
|
472
|
+
// access ("Arbeitskonto (OpenRouter)"). Do not propose dropping it again.
|
|
459
473
|
name: `${config.name} (${(_a = import_sign_in.PROVIDER_LABELS[config.provider]) != null ? _a : config.provider})`,
|
|
460
474
|
// The admin's object tree draws its connection icon from this link and
|
|
461
475
|
// from nothing else — govee, beszel, homewizard and nut2 all do the same.
|
|
@@ -497,7 +511,14 @@ class PollEngine {
|
|
|
497
511
|
{
|
|
498
512
|
id: `${config.id}.info.lastUpdate`,
|
|
499
513
|
type: "state",
|
|
500
|
-
common: {
|
|
514
|
+
common: {
|
|
515
|
+
name: (0, import_i18n.tName)("nameLastUpdate"),
|
|
516
|
+
desc: (0, import_i18n.tName)("descLastUpdate"),
|
|
517
|
+
type: "string",
|
|
518
|
+
role: "date",
|
|
519
|
+
read: true,
|
|
520
|
+
write: false
|
|
521
|
+
}
|
|
501
522
|
},
|
|
502
523
|
{
|
|
503
524
|
id: `${config.id}.warning`,
|
|
@@ -541,6 +562,7 @@ class PollEngine {
|
|
|
541
562
|
type: "state",
|
|
542
563
|
common: {
|
|
543
564
|
name: (0, import_i18n.tName)("nameTotalCostsToday"),
|
|
565
|
+
desc: (0, import_i18n.tName)("descTotalCosts"),
|
|
544
566
|
type: "number",
|
|
545
567
|
role: "value",
|
|
546
568
|
read: true,
|
|
@@ -553,6 +575,7 @@ class PollEngine {
|
|
|
553
575
|
type: "state",
|
|
554
576
|
common: {
|
|
555
577
|
name: (0, import_i18n.tName)("nameTotalCostsMonth"),
|
|
578
|
+
desc: (0, import_i18n.tName)("descTotalCosts"),
|
|
556
579
|
type: "number",
|
|
557
580
|
role: "value",
|
|
558
581
|
read: true,
|
|
@@ -565,7 +588,7 @@ class PollEngine {
|
|
|
565
588
|
type: "state",
|
|
566
589
|
common: {
|
|
567
590
|
name: (0, import_i18n.tName)("nameTotalCostsProjected"),
|
|
568
|
-
desc: (0, import_i18n.tName)("
|
|
591
|
+
desc: (0, import_i18n.tName)("descTotalCostsProjected"),
|
|
569
592
|
type: "number",
|
|
570
593
|
role: "value",
|
|
571
594
|
read: true,
|
|
@@ -616,12 +639,26 @@ class PollEngine {
|
|
|
616
639
|
{
|
|
617
640
|
id: "total.accountsReachable",
|
|
618
641
|
type: "state",
|
|
619
|
-
common: {
|
|
642
|
+
common: {
|
|
643
|
+
name: (0, import_i18n.tName)("nameTotalReachable"),
|
|
644
|
+
desc: (0, import_i18n.tName)("descTotalReachable"),
|
|
645
|
+
type: "number",
|
|
646
|
+
role: "value",
|
|
647
|
+
read: true,
|
|
648
|
+
write: false
|
|
649
|
+
}
|
|
620
650
|
},
|
|
621
651
|
{
|
|
622
652
|
id: "total.accounts",
|
|
623
653
|
type: "state",
|
|
624
|
-
common: {
|
|
654
|
+
common: {
|
|
655
|
+
name: (0, import_i18n.tName)("nameTotalAccounts"),
|
|
656
|
+
desc: (0, import_i18n.tName)("descTotalAccounts"),
|
|
657
|
+
type: "number",
|
|
658
|
+
role: "value",
|
|
659
|
+
read: true,
|
|
660
|
+
write: false
|
|
661
|
+
}
|
|
625
662
|
}
|
|
626
663
|
];
|
|
627
664
|
for (const def of defs) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/poll-engine.ts"],
|
|
4
|
-
"sourcesContent": ["import { tName } from \"./i18n\";\nimport type { AccountConfig } from \"./pure-helpers\";\nimport { PROVIDER_LABELS } from \"./sign-in\";\nimport { FetchError, type UsageProvider, type UsageSnapshot } from \"./provider\";\nimport { limitingWindow, lockedWindows, mapSnapshot, orphanObjectIds, type ObjectDef } from \"./snapshot-tree\";\nimport { computeTotals, type AccountStatus } from \"./totals\";\n\n/**\n * What the adapter currently knows about one account, in one word.\n *\n * `ok` and `rate-limited` mean the AI service is up and talking to us,\n * `unauthorized` means it is up but rejects our sign-in, `service-down` means the\n * service itself answered with a fault, `no-connection` means we never reached it.\n */\nexport type AccountState = \"ok\" | \"unauthorized\" | \"rate-limited\" | \"service-down\" | \"no-connection\" | \"not-signed-in\";\n\n/**\n * Whether an account is delivering usable numbers right now.\n *\n * ONE answer for the whole engine. The connection icon read it off the account's\n * state while the totals and `info.connection` read a second flag, so a first poll\n * that hit a throttle left the account green next to \"0 accounts reachable\".\n *\n * A throttle keeps the last values and the service is fine \u2014 that stays green; a\n * missing sign-in, a rejected one, a broken service and no connection do not.\n *\n * @param state the account's state\n * @returns true while the account counts as delivering\n */\nexport function isDelivering(state: AccountState): boolean {\n return state === \"ok\" || state === \"rate-limited\";\n}\n\n/** Consecutive network failures after which an account is judged unreachable. */\nconst MAX_NETWORK_FAILURES = 3;\n/** First backoff after a rate-limit answer (ms); doubles per repeat. */\nconst BACKOFF_START_MS = 10 * 60 * 1000;\n/** Backoff ceiling (ms). */\nconst BACKOFF_MAX_MS = 60 * 60 * 1000;\n/** Stagger between the accounts' first polls (ms) so they never fire in one burst. */\nconst STAGGER_MS = 3000;\n\n/**\n * What `info.error` says while the adapter itself has nothing to report: switched\n * off, or started and not asked yet.\n *\n * ONE wording for the whole fleet (krobi 2026-08-27) \u2014 the datapoint otherwise ends\n * up saying something different in every adapter. It stays a single word: the field\n * names the reason, it does not explain itself.\n */\nconst REASON_UNKNOWN = \"Unknown\";\n\n/** What `info.error` says for an account the adapter has no usable credential for. */\nconst NO_CREDENTIAL_REASON = \"No API key selected \u2014 pick one in the instance settings\";\n\n/** The adapter callbacks the engine drives \u2014 narrow, so tests need no adapter mock. */\nexport interface EngineDeps {\n /** Create or update an object. */\n upsertObject(def: ObjectDef): Promise<void>;\n /** Delete an object and everything below it. */\n deleteObject(id: string): Promise<void>;\n /** Every state id that currently exists below `prefix` (relative to the instance). */\n listStateIds(prefix: string): Promise<string[]>;\n /** Write a state value with ack \u2014 for MEASUREMENTS, where every cycle carries information. */\n setState(id: string, value: boolean | number | string): void;\n /**\n * Write a state only when the value differs from what the database holds \u2014 for\n * INDICATORS. js-controller does the comparison (`setStateChangedAsync`), which is\n * what the rest of the fleet uses; a hand-rolled cache would only know what this\n * process wrote and would still write blindly after a restart.\n *\n * Returns a promise so the shutdown path can WAIT for its writes; everywhere else\n * it is deliberately ignored \u2014 a poll cycle must not be held up by the database.\n */\n setStateChanged(id: string, value: boolean | number | string): Promise<void>;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n /**\n * Report whether an account's stored sign-in was REJECTED by the provider.\n *\n * Called on every state change of that fact, so the settings page can tell a\n * live sign-in from a token file that merely still exists. Without it the card\n * read \"signed in\" off file existence alone and showed a green check next to an\n * amber \"Sign-in rejected\" badge.\n *\n * @param accountId the account's object id\n * @param rejected true when the last query failed with `auth`\n */\n authState?(accountId: string, rejected: boolean): void;\n /**\n * Called once, when every account has finished its FIRST poll.\n *\n * The first round is staggered on purpose, so the adapter cannot report what the\n * object tree gained until the last account has been through. A config change\n * restarts the instance, so this is also the only moment a user needs the report.\n */\n afterFirstRound?(): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n /**\n * The account's source, or null when there is none to build \u2014 a key account\n * whose credential is missing or unreadable.\n *\n * Such an account still gets its objects and its start stamp. Skipping it\n * outright left the tree of a previously working account standing with its last\n * values AND its last status, so a key that had been removed showed as online\n * for as long as nobody looked (decision 19c applies to every account, not only\n * to the ones that can be polled).\n */\n provider: UsageProvider | null;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Whether the last query was rejected as unauthorized \u2014 reported to the sign-in card. */\n authRejected: boolean;\n /** Whether a plan-wide window was reported as CLOSED by the provider on the last answer. */\n locked: boolean;\n /** Whether the AI service itself answered on the last attempt. */\n serviceOnline: boolean;\n /** The account's one-word state. */\n state: AccountState;\n /** Plain-text reason shown in `info.error`; empty while everything works. */\n error: string;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n /** Whether this account has been through its first poll. */\n firstPollDone: boolean;\n /** True while a poll of this account is in flight \u2014 a second one must not overlap. */\n polling: boolean;\n /** Set when a poll was requested while one was running; runs once the current one ends. */\n pollAgain: boolean;\n /**\n * The dynamic state ids the last snapshot delivered, or null until the first\n * reconcile \u2014 which reads the database, so a datapoint that vanished while the\n * adapter was stopped is caught too.\n */\n deliveredIds: string[] | null;\n /** The skeleton's own state ids \u2014 they never expire. */\n staticIds: string[];\n}\n\n/**\n * Drives the polling of all configured accounts: staggered starts, one independent\n * cycle per account, typed failure handling (auth = immediate + one notification,\n * rate-limit = backoff keeping last values, service = reported down at once,\n * network = tolerated three times), warn-threshold transitions on the PLAN-WIDE\n * windows only, and the adapter-wide totals. Pure orchestration \u2014 all IO is injected.\n */\nexport class PollEngine {\n private readonly runtimes: AccountRuntime[] = [];\n private readonly handles: unknown[] = [];\n private stopped = false;\n private firstRoundReported = false;\n /**\n * How many accounts the user switched on \u2014 including those the adapter cannot\n * poll because their credential is missing. `total.accounts` is what the user\n * configured, not what happened to work out.\n */\n private readonly configuredAccounts: number;\n\n /**\n * @param accounts the validated account configs\n * @param providers each account id's provider (accounts without one are skipped)\n * @param intervalSec the poll interval in seconds\n * @param deps the injected adapter callbacks\n */\n public constructor(\n accounts: readonly AccountConfig[],\n providers: ReadonlyMap<string, UsageProvider>,\n private readonly intervalSec: number,\n private readonly deps: EngineDeps,\n ) {\n this.configuredAccounts = accounts.length;\n for (const config of accounts) {\n const provider = providers.get(config.id) ?? null;\n if (!provider) {\n // No warning here: the adapter already said which credential it could not\n // read, with the reason. A second line would only repeat it.\n deps.log.debug(`${config.name}: no usable credential \u2014 the account is shown as not delivering`);\n }\n this.runtimes.push({\n config,\n provider,\n status: { reachable: false, warning: false },\n failCount: 0,\n skipUntil: 0,\n backoffMs: BACKOFF_START_MS,\n authNotified: false,\n authRejected: false,\n serviceOnline: false,\n state: provider ? \"no-connection\" : \"not-signed-in\",\n // Nothing known until the service says something; the skeleton writes\n // REASON_UNKNOWN, and the first answer replaces it. An account without a\n // credential has its answer already.\n error: provider ? REASON_UNKNOWN : NO_CREDENTIAL_REASON,\n locked: false,\n createdObjects: new Set(),\n firstPollDone: false,\n polling: false,\n pollAgain: false,\n deliveredIds: null,\n staticIds: [],\n });\n }\n }\n\n /** Create the static per-account and totals objects, then arm the poll cycles. */\n public async start(): Promise<void> {\n for (const runtime of this.runtimes) {\n await this.createAccountSkeleton(runtime);\n }\n await this.createTotalsSkeleton();\n this.writeTotals();\n if (this.runtimes.length === 0) {\n // Nothing will ever poll \u2014 report right away, the cleanup may still have changed something.\n this.reportFirstRoundOnce();\n return;\n }\n this.runtimes.forEach((runtime, index) => {\n if (!runtime.provider) {\n // Nothing to ask. The skeleton above has already said so; count it as\n // been-through so it cannot hold the first-round report back for ever.\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n return;\n }\n // The repeating timer is armed INSIDE the staggered first poll, not next to\n // it: armed here it would start counting for every account in the same\n // instant, and from the second round on all of them would fire together \u2014\n // exactly the burst the stagger exists to prevent, against providers that\n // answer a burst by locking the whole account for a day.\n this.handles.push(\n this.deps.scheduleOnce(() => {\n if (this.stopped) {\n return;\n }\n this.handles.push(this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000));\n void this.pollAccount(runtime);\n }, index * STAGGER_MS),\n );\n });\n }\n\n /** Cancel every timer. Synchronous \u2014 safe from onUnload. */\n public stop(): void {\n this.stopped = true;\n for (const handle of this.handles) {\n this.deps.cancel(handle);\n }\n this.handles.length = 0;\n }\n\n /**\n * Say that no account is delivering any more \u2014 for shutdown.\n *\n * A stopped adapter reads nothing, so it must not leave every account claiming to\n * be online: `info.unreach` is what colours the account in the admin's object tree\n * and the badge in the settings, and on its last value it stays green for as long\n * as the instance is switched off.\n *\n * `info.error` goes back to {@link REASON_UNKNOWN}: while the instance is off the\n * adapter has nothing to report, and the fleet-wide word for that is the single\n * word \"Unknown\" (krobi 2026-08-27) \u2014 never a stale provider message, never an\n * invented sentence about our own operating state.\n *\n * The returned promise is what makes this WORK. Measured on the live server\n * 2026-08-27: issued fire-and-forget and followed by an immediate `callback()`,\n * not one of these writes ever reached the database \u2014 the process was gone first.\n * The caller has to wait for this before telling the host it is done \u2014 the host's\n * own deadline is the only limit there is.\n *\n * @returns resolves once every write has been acknowledged\n */\n public async markAllOffline(): Promise<void> {\n const writes = this.runtimes.flatMap(runtime => [\n this.deps.setStateChanged(`${runtime.config.id}.info.unreach`, true),\n this.deps.setStateChanged(`${runtime.config.id}.info.error`, REASON_UNKNOWN),\n ]);\n // The same lie one level up: \"accounts currently delivering data\" is zero.\n writes.push(this.deps.setStateChanged(\"total.accountsReachable\", 0));\n writes.push(this.deps.setStateChanged(\"info.connection\", false));\n await Promise.all(writes);\n }\n\n /**\n * Poll one account immediately, by id. Used after a successful sign-in: waiting\n * up to a full interval there reads as \"the sign-in did not work\".\n *\n * @param accountId the account's object id\n */\n public async pollNow(accountId: string): Promise<void> {\n const runtime = this.runtimes.find(entry => entry.config.id === accountId);\n if (runtime) {\n // A fresh sign-in clears a previous auth failure and any backoff.\n runtime.authNotified = false;\n runtime.authRejected = false;\n runtime.skipUntil = 0;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * Poll one account now (also used by the staggered first run).\n *\n * Never two at once for the same account: a sign-in triggers an immediate poll,\n * which can land on top of a scheduled one \u2014 and two token refreshes in parallel\n * on a rotating refresh token sign each other out. A request that arrives while\n * one is running is remembered and runs right after, so nothing is lost.\n *\n * @param runtime the account's runtime\n */\n private async pollAccount(runtime: AccountRuntime): Promise<void> {\n if (this.stopped || !runtime.provider) {\n return;\n }\n if (runtime.polling) {\n runtime.pollAgain = true;\n return;\n }\n runtime.polling = true;\n try {\n await this.pollOnce(runtime);\n } finally {\n runtime.polling = false;\n }\n if (runtime.pollAgain && !this.stopped) {\n runtime.pollAgain = false;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * One poll of one account: fetch, classify, write.\n *\n * @param runtime the account's runtime\n */\n private async pollOnce(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n if (this.deps.now() < runtime.skipUntil) {\n this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);\n // Still counts as \"been through\": otherwise an account that starts inside a\n // backoff would hold the first-round report back forever.\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n return;\n }\n const wasFailing = runtime.state !== \"ok\";\n try {\n const snapshot = await (runtime.provider as UsageProvider).fetch();\n // The answer can arrive AFTER onUnload: `stop()` cancels the timers, it\n // cannot cancel a request already in flight. Writing now would undo\n // `markAllOffline()` and leave a stopped adapter claiming the account is\n // online \u2014 the exact state decision 19 exists to prevent.\n if (this.stopped) {\n return;\n }\n runtime.failCount = 0;\n runtime.backoffMs = BACKOFF_START_MS;\n runtime.authNotified = false;\n runtime.status.snapshot = snapshot;\n runtime.serviceOnline = true;\n runtime.state = \"ok\";\n runtime.error = \"\";\n if (runtime.authRejected) {\n runtime.authRejected = false;\n this.deps.authState?.(config.id, false);\n }\n if (wasFailing) {\n // The failure said so in the log; the recovery has to as well, or the\n // last word on this account stays a warning that is no longer true.\n this.deps.log.info(`${config.name}: delivering again`);\n }\n await this.applySnapshot(runtime, snapshot);\n } catch (e) {\n if (this.stopped) {\n return;\n }\n this.handleFailure(runtime, e);\n }\n runtime.status.reachable = isDelivering(runtime.state);\n this.writeAccountInfo(runtime);\n this.writeTotals();\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n }\n\n /** Fire the first-round hook exactly once, when no account is still pending. */\n private reportFirstRoundOnce(): void {\n if (this.firstRoundReported || this.runtimes.some(runtime => !runtime.firstPollDone)) {\n return;\n }\n this.firstRoundReported = true;\n this.deps.afterFirstRound?.();\n }\n\n /**\n * Write a successful snapshot: upsert new objects (create-once cache), write the\n * values, and run the warn-threshold transition.\n *\n * @param runtime the account's runtime\n * @param snapshot the fetched snapshot\n */\n private async applySnapshot(runtime: AccountRuntime, snapshot: UsageSnapshot): Promise<void> {\n const { config } = runtime;\n const { objects, writes } = mapSnapshot(config.id, snapshot);\n for (const object of objects) {\n if (!runtime.createdObjects.has(object.id)) {\n await this.deps.upsertObject(object);\n runtime.createdObjects.add(object.id);\n }\n }\n for (const write of writes) {\n this.deps.setState(write.id, write.value);\n }\n await this.removeVanished(\n runtime,\n writes.map(write => write.id),\n );\n // Only PLAN-WIDE windows speak for the account \u2014 a per-model bucket at 100 %\n // must not read as \"this AI is full\" (krobi 2026-08-26, again 2026-09-06:\n // \"fable 100% ist das fable limit, aber weder das 5h stunden limit noch das\n // wochenlimit\").\n const driver = limitingWindow(snapshot);\n const percent = driver?.percent ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n // A provider that CLOSED a plan-wide window has said outright what the\n // percentage only implies. Both count.\n const locked = lockedWindows(snapshot);\n if (locked.length > 0 && !runtime.locked) {\n this.deps.log.warn(`${config.name}: ${locked[0].label} is locked by the provider \u2014 ${locked[0].reason}`);\n }\n runtime.locked = locked.length > 0;\n // Indicators go through the changed-write, measurements through the normal one.\n void this.deps.setStateChanged(`${config.id}.warning`, runtime.status.warning);\n void this.deps.setStateChanged(`${config.id}.limitReached`, percent >= 100 || runtime.locked);\n if (runtime.status.warning && !wasWarning) {\n // Always name the window: \"usage at 100 %\" without it was misleading whenever\n // several windows existed.\n const window = driver ? `${driver.label} ` : \"\";\n const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Delete what this account no longer delivers.\n *\n * The first round after a start compares against the DATABASE, so a window or\n * model that disappeared while the adapter was stopped is caught as well; every\n * round after that compares against the previous snapshot, which costs nothing.\n *\n * @param runtime the account's runtime\n * @param delivered the state ids this snapshot wrote\n */\n private async removeVanished(runtime: AccountRuntime, delivered: string[]): Promise<void> {\n const known = runtime.deliveredIds ?? (await this.deps.listStateIds(runtime.config.id));\n for (const id of orphanObjectIds(known, delivered, runtime.staticIds)) {\n await this.deps.deleteObject(id);\n runtime.createdObjects.delete(id);\n this.deps.log.info(`${runtime.config.name}: removed \"${id}\" \u2014 the provider no longer reports it`);\n }\n runtime.deliveredIds = delivered;\n }\n\n /**\n * Classify a fetch failure.\n *\n * The split matters for the online indicator: with `auth` and `rate-limit` the AI\n * service ANSWERED \u2014 it is online, it just said no \u2014 so only our own access is\n * broken. `service` means the service answered with a fault of its own and is\n * reported as down at once (it told us, that is not a flake). A `network` failure\n * is tolerated MAX_NETWORK_FAILURES times before we call the connection gone, so\n * a single hiccup does not make the indicator flap.\n *\n * @param runtime the account's runtime\n * @param error the thrown error\n */\n private handleFailure(runtime: AccountRuntime, error: unknown): void {\n const { config } = runtime;\n const message = error instanceof Error ? error.message : String(error);\n if (error instanceof FetchError && error.kind === \"no-credentials\") {\n // Nobody has signed in yet, or the key is gone. That is not a rejected\n // sign-in: no notification, no warning, and the settings page keeps\n // offering the sign-in button instead of an error.\n runtime.serviceOnline = true;\n runtime.state = \"not-signed-in\";\n runtime.error = message;\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n this.deps.log.info(`${config.name}: ${message}`);\n }\n return;\n }\n if (error instanceof FetchError && error.kind === \"auth\") {\n runtime.serviceOnline = true;\n runtime.state = \"unauthorized\";\n runtime.error = `Sign-in rejected \u2014 ${message}`;\n if (!runtime.authRejected) {\n runtime.authRejected = true;\n this.deps.authState?.(config.id, true);\n }\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n const text = `${config.name}: credentials rejected \u2014 ${message}`;\n this.deps.log.warn(text);\n this.deps.notify?.(config.name, text);\n }\n return;\n }\n\n if (error instanceof FetchError && error.kind === \"rate-limit\") {\n runtime.serviceOnline = true;\n runtime.state = \"rate-limited\";\n runtime.error = `Throttled by the provider \u2014 retrying in ${Math.round(runtime.backoffMs / 60000)} min, last values kept`;\n runtime.skipUntil = this.deps.now() + runtime.backoffMs;\n this.deps.log.warn(\n `${config.name}: rate-limited \u2014 backing off for ${Math.round(runtime.backoffMs / 60000)} min, keeping last values`,\n );\n runtime.backoffMs = Math.min(BACKOFF_MAX_MS, runtime.backoffMs * 2);\n return;\n }\n if (error instanceof FetchError && error.kind === \"service\") {\n runtime.failCount = 0;\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: the service reports a fault (${message}) \u2014 values kept`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"service-down\";\n runtime.error = `The AI service reports a fault \u2014 ${message}`;\n return;\n }\n runtime.failCount++;\n this.deps.log.debug(`${config.name}: fetch failed (${message}), attempt ${runtime.failCount}`);\n if (runtime.failCount >= MAX_NETWORK_FAILURES) {\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: not reachable after ${runtime.failCount} attempts (${message})`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"no-connection\";\n runtime.error = `Not reachable after ${runtime.failCount} attempts \u2014 ${message}`;\n }\n }\n\n /**\n * Write one account's info states (offline marker, error text, last update).\n *\n * @param runtime the account's runtime\n */\n private writeAccountInfo(runtime: AccountRuntime): void {\n const { config } = runtime;\n void this.writeAccountStatus(runtime);\n if (runtime.status.reachable) {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /**\n * Write the two status states.\n *\n * `unreach` drives the connection icon the admin draws next to the account, so it\n * has to mean what a user reads into that icon: green while the account delivers.\n * A throttle keeps the last values and the service is fine, so it stays green and\n * only fills the error text; a dead sign-in, a broken service or no connection at\n * all turn it off. Both go through the changed-write: an indicator rewritten every\n * cycle floods the history and hides the real transition.\n *\n * @param runtime the account's runtime\n */\n private async writeAccountStatus(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const delivering = isDelivering(runtime.state);\n await Promise.all([\n this.deps.setStateChanged(`${config.id}.info.unreach`, !delivering),\n this.deps.setStateChanged(`${config.id}.info.error`, runtime.error),\n ]);\n }\n\n /** Recompute and write the totals + info.connection. */\n private writeTotals(): void {\n const totals = computeTotals(\n this.runtimes.map(runtime => runtime.status),\n this.configuredAccounts,\n );\n // Every total goes through the changed-write. They are recomputed after EVERY\n // account's poll \u2014 with several accounts that meant one write per account per\n // round, almost always of the value that was already there. What they say only\n // changes when an account's numbers change, and then the write happens.\n void this.deps.setStateChanged(\"total.costs.today\", totals.costsToday);\n void this.deps.setStateChanged(\"total.costs.month\", totals.costsMonth);\n void this.deps.setStateChanged(\"total.costs.projectedMonth\", totals.costsProjectedMonth);\n void this.deps.setStateChanged(\"total.maxLimitPercent\", totals.maxLimitPercent);\n void this.deps.setStateChanged(\"total.warningsActive\", totals.warningsActive);\n void this.deps.setStateChanged(\"total.limitReached\", totals.limitReached);\n void this.deps.setStateChanged(\"total.accountsReachable\", totals.accountsReachable);\n // The configured count only ever changes with the configuration, which restarts\n // the instance \u2014 rewriting it every cycle would be pure noise in a recording.\n void this.deps.setStateChanged(\"total.accounts\", totals.accounts);\n void this.deps.setStateChanged(\"info.connection\", totals.accountsReachable > 0);\n }\n\n /**\n * The static per-account objects that exist regardless of what the source delivers.\n *\n * @param runtime the account's runtime\n */\n private async createAccountSkeleton(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const defs: ObjectDef[] = [\n {\n id: config.id,\n type: \"device\",\n common: {\n // The readable provider name, never the internal kind: the node used to\n // read \"Claude (claude-sub)\" and \"My key (anthropic-api)\" in the tree.\n name: `${config.name} (${PROVIDER_LABELS[config.provider] ?? config.provider})`,\n // The admin's object tree draws its connection icon from this link and\n // from nothing else \u2014 govee, beszel, homewizard and nut2 all do the same.\n statusStates: { offlineId: \"info.unreach\" },\n },\n },\n { id: `${config.id}.info`, type: \"channel\", common: { name: tName(\"nameAccountInfo\") } },\n {\n // The two slots ioBroker itself provides for this \u2014 measured against\n // @iobroker/type-detector 6.0.0: `unreach` is the offline marker every\n // device type carries (`indicator.reachable` is deprecated there), and\n // `indicator.error` is the standard home for the reason.\n id: `${config.id}.info.unreach`,\n type: \"state\",\n common: {\n name: tName(\"nameUnreach\"),\n desc: tName(\"descUnreach\"),\n type: \"boolean\",\n role: \"indicator.maintenance.unreach\",\n read: true,\n write: false,\n },\n },\n {\n // NOT `indicator.error`: the two official sources disagree \u2014 the type-detector\n // lists that role as a String, the repochecker's validity whitelist allows\n // boolean only (E1009). Validity wins, so the message rides on `text`.\n id: `${config.id}.info.error`,\n type: \"state\",\n common: {\n name: tName(\"nameLastError\"),\n desc: tName(\"descLastError\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n },\n },\n {\n id: `${config.id}.info.lastUpdate`,\n type: \"state\",\n common: { name: tName(\"nameLastUpdate\"), type: \"string\", role: \"date\", read: true, write: false },\n },\n {\n id: `${config.id}.warning`,\n type: \"state\",\n common: {\n name: tName(\"nameWarning\"),\n desc: tName(\"descWarning\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n {\n id: `${config.id}.limitReached`,\n type: \"state\",\n common: {\n name: tName(\"nameLimitReached\"),\n desc: tName(\"descLimitReached\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n runtime.createdObjects.add(def.id);\n }\n runtime.staticIds = defs.filter(def => def.type === \"state\").map(def => def.id);\n // Mark it as not delivering right away, before anything has been asked.\n //\n // This looks pessimistic and is the honest state: nothing has been read yet. It\n // also carries the whole weight of \"the instance was off\". Whatever the previous\n // run left behind stands until someone overwrites it \u2014 after a hard kill, after\n // a crash, after an unclean shutdown the account would otherwise sit there green\n // and claim to deliver while no process exists at all. nut2 marks its devices\n // unreachable on start for exactly this reason.\n //\n // The window is short: the first poll follows within seconds and writes the real\n // state, success or failure. Only the marker \u2014 `info.error` belongs to the AI\n // service, and \"we have not asked yet\" is not something it said.\n void this.deps.setStateChanged(`${config.id}.info.unreach`, true);\n // REASON_UNKNOWN while nothing has been asked yet \u2014 but an account without a\n // usable credential has its answer already, and repeating \"Unknown\" there\n // would hide it.\n void this.deps.setStateChanged(`${config.id}.info.error`, runtime.error);\n }\n\n /** The totals skeleton (channel + states). */\n private async createTotalsSkeleton(): Promise<void> {\n const defs: ObjectDef[] = [\n { id: \"total.costs\", type: \"channel\", common: { name: tName(\"nameTotalCosts\") } },\n {\n id: \"total.costs.today\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalCostsToday\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.month\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalCostsMonth\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.projectedMonth\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalCostsProjected\"),\n desc: tName(\"descCostsProjected\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.maxLimitPercent\",\n type: \"state\",\n common: {\n // NOT \"plan-wide utilisation\": the value is whatever speaks for an\n // account (fullest plan-wide window OR its granted budget, see\n // limitingWindow) \u2014 the old name promised something narrower than the\n // number ever was.\n name: tName(\"nameTotalMaxPercent\"),\n desc: tName(\"descTotalMaxPercent\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"%\",\n },\n },\n {\n id: \"total.warningsActive\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalWarnings\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.limitReached\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalLimitReached\"),\n desc: tName(\"descLimitReached\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.accountsReachable\",\n type: \"state\",\n common: { name: tName(\"nameTotalReachable\"), type: \"number\", role: \"value\", read: true, write: false },\n },\n {\n id: \"total.accounts\",\n type: \"state\",\n common: { name: tName(\"nameTotalAccounts\"), type: \"number\", role: \"value\", read: true, write: false },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n }\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAsB;AAEtB,qBAAgC;AAChC,sBAAmE;AACnE,2BAA4F;AAC5F,oBAAkD;AAwB3C,SAAS,aAAa,OAA8B;AACzD,SAAO,UAAU,QAAQ,UAAU;AACrC;AAGA,MAAM,uBAAuB;AAE7B,MAAM,mBAAmB,KAAK,KAAK;AAEnC,MAAM,iBAAiB,KAAK,KAAK;AAEjC,MAAM,aAAa;AAUnB,MAAM,iBAAiB;AAGvB,MAAM,uBAAuB;AAkHtB,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBf,YACL,UACA,WACiB,aACA,MACjB;AAFiB;AACA;AA7LrB;AA+LI,SAAK,qBAAqB,SAAS;AACnC,eAAW,UAAU,UAAU;AAC7B,YAAM,YAAW,eAAU,IAAI,OAAO,EAAE,MAAvB,YAA4B;AAC7C,UAAI,CAAC,UAAU;AAGb,aAAK,IAAI,MAAM,GAAG,OAAO,IAAI,sEAAiE;AAAA,MAChG;AACA,WAAK,SAAS,KAAK;AAAA,QACjB;AAAA,QACA;AAAA,QACA,QAAQ,EAAE,WAAW,OAAO,SAAS,MAAM;AAAA,QAC3C,WAAW;AAAA,QACX,WAAW;AAAA,QACX,WAAW;AAAA,QACX,cAAc;AAAA,QACd,cAAc;AAAA,QACd,eAAe;AAAA,QACf,OAAO,WAAW,kBAAkB;AAAA;AAAA;AAAA;AAAA,QAIpC,OAAO,WAAW,iBAAiB;AAAA,QACnC,QAAQ;AAAA,QACR,gBAAgB,oBAAI,IAAI;AAAA,QACxB,eAAe;AAAA,QACf,SAAS;AAAA,QACT,WAAW;AAAA,QACX,cAAc;AAAA,QACd,WAAW,CAAC;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAnCmB;AAAA,EACA;AAAA,EArBF,WAA6B,CAAC;AAAA,EAC9B,UAAqB,CAAC;AAAA,EAC/B,UAAU;AAAA,EACV,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMZ;AAAA;AAAA,EAiDjB,MAAa,QAAuB;AAClC,eAAW,WAAW,KAAK,UAAU;AACnC,YAAM,KAAK,sBAAsB,OAAO;AAAA,IAC1C;AACA,UAAM,KAAK,qBAAqB;AAChC,SAAK,YAAY;AACjB,QAAI,KAAK,SAAS,WAAW,GAAG;AAE9B,WAAK,qBAAqB;AAC1B;AAAA,IACF;AACA,SAAK,SAAS,QAAQ,CAAC,SAAS,UAAU;AACxC,UAAI,CAAC,QAAQ,UAAU;AAGrB,gBAAQ,gBAAgB;AACxB,aAAK,qBAAqB;AAC1B;AAAA,MACF;AAMA,WAAK,QAAQ;AAAA,QACX,KAAK,KAAK,aAAa,MAAM;AAC3B,cAAI,KAAK,SAAS;AAChB;AAAA,UACF;AACA,eAAK,QAAQ,KAAK,KAAK,KAAK,SAAS,MAAM,KAAK,KAAK,YAAY,OAAO,GAAG,KAAK,cAAc,GAAI,CAAC;AACnG,eAAK,KAAK,YAAY,OAAO;AAAA,QAC/B,GAAG,QAAQ,UAAU;AAAA,MACvB;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGO,OAAa;AAClB,SAAK,UAAU;AACf,eAAW,UAAU,KAAK,SAAS;AACjC,WAAK,KAAK,OAAO,MAAM;AAAA,IACzB;AACA,SAAK,QAAQ,SAAS;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,MAAa,iBAAgC;AAC3C,UAAM,SAAS,KAAK,SAAS,QAAQ,aAAW;AAAA,MAC9C,KAAK,KAAK,gBAAgB,GAAG,QAAQ,OAAO,EAAE,iBAAiB,IAAI;AAAA,MACnE,KAAK,KAAK,gBAAgB,GAAG,QAAQ,OAAO,EAAE,eAAe,cAAc;AAAA,IAC7E,CAAC;AAED,WAAO,KAAK,KAAK,KAAK,gBAAgB,2BAA2B,CAAC,CAAC;AACnE,WAAO,KAAK,KAAK,KAAK,gBAAgB,mBAAmB,KAAK,CAAC;AAC/D,UAAM,QAAQ,IAAI,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAa,QAAQ,WAAkC;AACrD,UAAM,UAAU,KAAK,SAAS,KAAK,WAAS,MAAM,OAAO,OAAO,SAAS;AACzE,QAAI,SAAS;AAEX,cAAQ,eAAe;AACvB,cAAQ,eAAe;AACvB,cAAQ,YAAY;AACpB,YAAM,KAAK,YAAY,OAAO;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,YAAY,SAAwC;AAChE,QAAI,KAAK,WAAW,CAAC,QAAQ,UAAU;AACrC;AAAA,IACF;AACA,QAAI,QAAQ,SAAS;AACnB,cAAQ,YAAY;AACpB;AAAA,IACF;AACA,YAAQ,UAAU;AAClB,QAAI;AACF,YAAM,KAAK,SAAS,OAAO;AAAA,IAC7B,UAAE;AACA,cAAQ,UAAU;AAAA,IACpB;AACA,QAAI,QAAQ,aAAa,CAAC,KAAK,SAAS;AACtC,cAAQ,YAAY;AACpB,YAAM,KAAK,YAAY,OAAO;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,SAAS,SAAwC;AAnWjE;AAoWI,UAAM,EAAE,OAAO,IAAI;AACnB,QAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,WAAW;AACvC,WAAK,KAAK,IAAI,MAAM,GAAG,OAAO,IAAI,6CAAwC;AAG1E,cAAQ,gBAAgB;AACxB,WAAK,qBAAqB;AAC1B;AAAA,IACF;AACA,UAAM,aAAa,QAAQ,UAAU;AACrC,QAAI;AACF,YAAM,WAAW,MAAO,QAAQ,SAA2B,MAAM;AAKjE,UAAI,KAAK,SAAS;AAChB;AAAA,MACF;AACA,cAAQ,YAAY;AACpB,cAAQ,YAAY;AACpB,cAAQ,eAAe;AACvB,cAAQ,OAAO,WAAW;AAC1B,cAAQ,gBAAgB;AACxB,cAAQ,QAAQ;AAChB,cAAQ,QAAQ;AAChB,UAAI,QAAQ,cAAc;AACxB,gBAAQ,eAAe;AACvB,yBAAK,MAAK,cAAV,4BAAsB,OAAO,IAAI;AAAA,MACnC;AACA,UAAI,YAAY;AAGd,aAAK,KAAK,IAAI,KAAK,GAAG,OAAO,IAAI,oBAAoB;AAAA,MACvD;AACA,YAAM,KAAK,cAAc,SAAS,QAAQ;AAAA,IAC5C,SAAS,GAAG;AACV,UAAI,KAAK,SAAS;AAChB;AAAA,MACF;AACA,WAAK,cAAc,SAAS,CAAC;AAAA,IAC/B;AACA,YAAQ,OAAO,YAAY,aAAa,QAAQ,KAAK;AACrD,SAAK,iBAAiB,OAAO;AAC7B,SAAK,YAAY;AACjB,YAAQ,gBAAgB;AACxB,SAAK,qBAAqB;AAAA,EAC5B;AAAA;AAAA,EAGQ,uBAA6B;AAtZvC;AAuZI,QAAI,KAAK,sBAAsB,KAAK,SAAS,KAAK,aAAW,CAAC,QAAQ,aAAa,GAAG;AACpF;AAAA,IACF;AACA,SAAK,qBAAqB;AAC1B,qBAAK,MAAK,oBAAV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,cAAc,SAAyB,UAAwC;AAra/F;AAsaI,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,EAAE,SAAS,OAAO,QAAI,kCAAY,OAAO,IAAI,QAAQ;AAC3D,eAAW,UAAU,SAAS;AAC5B,UAAI,CAAC,QAAQ,eAAe,IAAI,OAAO,EAAE,GAAG;AAC1C,cAAM,KAAK,KAAK,aAAa,MAAM;AACnC,gBAAQ,eAAe,IAAI,OAAO,EAAE;AAAA,MACtC;AAAA,IACF;AACA,eAAW,SAAS,QAAQ;AAC1B,WAAK,KAAK,SAAS,MAAM,IAAI,MAAM,KAAK;AAAA,IAC1C;AACA,UAAM,KAAK;AAAA,MACT;AAAA,MACA,OAAO,IAAI,WAAS,MAAM,EAAE;AAAA,IAC9B;AAKA,UAAM,aAAS,qCAAe,QAAQ;AACtC,UAAM,WAAU,sCAAQ,YAAR,YAAmB;AACnC,UAAM,aAAa,QAAQ,OAAO;AAClC,YAAQ,OAAO,UAAU,WAAW,OAAO;AAG3C,UAAM,aAAS,oCAAc,QAAQ;AACrC,QAAI,OAAO,SAAS,KAAK,CAAC,QAAQ,QAAQ;AACxC,WAAK,KAAK,IAAI,KAAK,GAAG,OAAO,IAAI,KAAK,OAAO,CAAC,EAAE,KAAK,qCAAgC,OAAO,CAAC,EAAE,MAAM,EAAE;AAAA,IACzG;AACA,YAAQ,SAAS,OAAO,SAAS;AAEjC,SAAK,KAAK,KAAK,gBAAgB,GAAG,OAAO,EAAE,YAAY,QAAQ,OAAO,OAAO;AAC7E,SAAK,KAAK,KAAK,gBAAgB,GAAG,OAAO,EAAE,iBAAiB,WAAW,OAAO,QAAQ,MAAM;AAC5F,QAAI,QAAQ,OAAO,WAAW,CAAC,YAAY;AAGzC,YAAM,SAAS,SAAS,GAAG,OAAO,KAAK,MAAM;AAC7C,YAAM,UAAU,GAAG,OAAO,IAAI,KAAK,MAAM,MAAM,KAAK,MAAM,OAAO,CAAC,iBAAiB,OAAO,aAAa;AACvG,WAAK,KAAK,IAAI,KAAK,OAAO;AAC1B,uBAAK,MAAK,WAAV,4BAAmB,OAAO,MAAM;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,eAAe,SAAyB,WAAoC;AA3d5F;AA4dI,UAAM,SAAQ,aAAQ,iBAAR,YAAyB,MAAM,KAAK,KAAK,aAAa,QAAQ,OAAO,EAAE;AACrF,eAAW,UAAM,sCAAgB,OAAO,WAAW,QAAQ,SAAS,GAAG;AACrE,YAAM,KAAK,KAAK,aAAa,EAAE;AAC/B,cAAQ,eAAe,OAAO,EAAE;AAChC,WAAK,KAAK,IAAI,KAAK,GAAG,QAAQ,OAAO,IAAI,cAAc,EAAE,4CAAuC;AAAA,IAClG;AACA,YAAQ,eAAe;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeQ,cAAc,SAAyB,OAAsB;AAlfvE;AAmfI,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,QAAI,iBAAiB,8BAAc,MAAM,SAAS,kBAAkB;AAIlE,cAAQ,gBAAgB;AACxB,cAAQ,QAAQ;AAChB,cAAQ,QAAQ;AAChB,UAAI,CAAC,QAAQ,cAAc;AACzB,gBAAQ,eAAe;AACvB,aAAK,KAAK,IAAI,KAAK,GAAG,OAAO,IAAI,KAAK,OAAO,EAAE;AAAA,MACjD;AACA;AAAA,IACF;AACA,QAAI,iBAAiB,8BAAc,MAAM,SAAS,QAAQ;AACxD,cAAQ,gBAAgB;AACxB,cAAQ,QAAQ;AAChB,cAAQ,QAAQ,2BAAsB,OAAO;AAC7C,UAAI,CAAC,QAAQ,cAAc;AACzB,gBAAQ,eAAe;AACvB,yBAAK,MAAK,cAAV,4BAAsB,OAAO,IAAI;AAAA,MACnC;AACA,UAAI,CAAC,QAAQ,cAAc;AACzB,gBAAQ,eAAe;AACvB,cAAM,OAAO,GAAG,OAAO,IAAI,iCAA4B,OAAO;AAC9D,aAAK,KAAK,IAAI,KAAK,IAAI;AACvB,yBAAK,MAAK,WAAV,4BAAmB,OAAO,MAAM;AAAA,MAClC;AACA;AAAA,IACF;AAEA,QAAI,iBAAiB,8BAAc,MAAM,SAAS,cAAc;AAC9D,cAAQ,gBAAgB;AACxB,cAAQ,QAAQ;AAChB,cAAQ,QAAQ,gDAA2C,KAAK,MAAM,QAAQ,YAAY,GAAK,CAAC;AAChG,cAAQ,YAAY,KAAK,KAAK,IAAI,IAAI,QAAQ;AAC9C,WAAK,KAAK,IAAI;AAAA,QACZ,GAAG,OAAO,IAAI,yCAAoC,KAAK,MAAM,QAAQ,YAAY,GAAK,CAAC;AAAA,MACzF;AACA,cAAQ,YAAY,KAAK,IAAI,gBAAgB,QAAQ,YAAY,CAAC;AAClE;AAAA,IACF;AACA,QAAI,iBAAiB,8BAAc,MAAM,SAAS,WAAW;AAC3D,cAAQ,YAAY;AACpB,UAAI,QAAQ,eAAe;AACzB,aAAK,KAAK,IAAI,KAAK,GAAG,OAAO,IAAI,kCAAkC,OAAO,sBAAiB;AAAA,MAC7F;AACA,cAAQ,gBAAgB;AACxB,cAAQ,QAAQ;AAChB,cAAQ,QAAQ,yCAAoC,OAAO;AAC3D;AAAA,IACF;AACA,YAAQ;AACR,SAAK,KAAK,IAAI,MAAM,GAAG,OAAO,IAAI,mBAAmB,OAAO,cAAc,QAAQ,SAAS,EAAE;AAC7F,QAAI,QAAQ,aAAa,sBAAsB;AAC7C,UAAI,QAAQ,eAAe;AACzB,aAAK,KAAK,IAAI,KAAK,GAAG,OAAO,IAAI,yBAAyB,QAAQ,SAAS,cAAc,OAAO,GAAG;AAAA,MACrG;AACA,cAAQ,gBAAgB;AACxB,cAAQ,QAAQ;AAChB,cAAQ,QAAQ,uBAAuB,QAAQ,SAAS,oBAAe,OAAO;AAAA,IAChF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,iBAAiB,SAA+B;AACtD,UAAM,EAAE,OAAO,IAAI;AACnB,SAAK,KAAK,mBAAmB,OAAO;AACpC,QAAI,QAAQ,OAAO,WAAW;AAC5B,WAAK,KAAK,SAAS,GAAG,OAAO,EAAE,oBAAoB,IAAI,KAAK,KAAK,KAAK,IAAI,CAAC,EAAE,YAAY,CAAC;AAAA,IAC5F;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAc,mBAAmB,SAAwC;AACvE,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,aAAa,aAAa,QAAQ,KAAK;AAC7C,UAAM,QAAQ,IAAI;AAAA,MAChB,KAAK,KAAK,gBAAgB,GAAG,OAAO,EAAE,iBAAiB,CAAC,UAAU;AAAA,MAClE,KAAK,KAAK,gBAAgB,GAAG,OAAO,EAAE,eAAe,QAAQ,KAAK;AAAA,IACpE,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,cAAoB;AAC1B,UAAM,aAAS;AAAA,MACb,KAAK,SAAS,IAAI,aAAW,QAAQ,MAAM;AAAA,MAC3C,KAAK;AAAA,IACP;AAKA,SAAK,KAAK,KAAK,gBAAgB,qBAAqB,OAAO,UAAU;AACrE,SAAK,KAAK,KAAK,gBAAgB,qBAAqB,OAAO,UAAU;AACrE,SAAK,KAAK,KAAK,gBAAgB,8BAA8B,OAAO,mBAAmB;AACvF,SAAK,KAAK,KAAK,gBAAgB,yBAAyB,OAAO,eAAe;AAC9E,SAAK,KAAK,KAAK,gBAAgB,wBAAwB,OAAO,cAAc;AAC5E,SAAK,KAAK,KAAK,gBAAgB,sBAAsB,OAAO,YAAY;AACxE,SAAK,KAAK,KAAK,gBAAgB,2BAA2B,OAAO,iBAAiB;AAGlF,SAAK,KAAK,KAAK,gBAAgB,kBAAkB,OAAO,QAAQ;AAChE,SAAK,KAAK,KAAK,gBAAgB,mBAAmB,OAAO,oBAAoB,CAAC;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,sBAAsB,SAAwC;AAlnB9E;AAmnBI,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,OAAoB;AAAA,MACxB;AAAA,QACE,IAAI,OAAO;AAAA,QACX,MAAM;AAAA,QACN,QAAQ;AAAA;AAAA;AAAA,UAGN,MAAM,GAAG,OAAO,IAAI,MAAK,oCAAgB,OAAO,QAAQ,MAA/B,YAAoC,OAAO,QAAQ;AAAA;AAAA;AAAA,UAG5E,cAAc,EAAE,WAAW,eAAe;AAAA,QAC5C;AAAA,MACF;AAAA,MACA,EAAE,IAAI,GAAG,OAAO,EAAE,SAAS,MAAM,WAAW,QAAQ,EAAE,UAAM,mBAAM,iBAAiB,EAAE,EAAE;AAAA,MACvF;AAAA;AAAA;AAAA;AAAA;AAAA,QAKE,IAAI,GAAG,OAAO,EAAE;AAAA,QAChB,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,aAAa;AAAA,UACzB,UAAM,mBAAM,aAAa;AAAA,UACzB,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA;AAAA;AAAA;AAAA;AAAA,QAIE,IAAI,GAAG,OAAO,EAAE;AAAA,QAChB,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,eAAe;AAAA,UAC3B,UAAM,mBAAM,eAAe;AAAA,UAC3B,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI,GAAG,OAAO,EAAE;AAAA,QAChB,MAAM;AAAA,QACN,QAAQ,EAAE,UAAM,mBAAM,gBAAgB,GAAG,MAAM,UAAU,MAAM,QAAQ,MAAM,MAAM,OAAO,MAAM;AAAA,MAClG;AAAA,MACA;AAAA,QACE,IAAI,GAAG,OAAO,EAAE;AAAA,QAChB,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,aAAa;AAAA,UACzB,UAAM,mBAAM,aAAa;AAAA,UACzB,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI,GAAG,OAAO,EAAE;AAAA,QAChB,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,kBAAkB;AAAA,UAC9B,UAAM,mBAAM,kBAAkB;AAAA,UAC9B,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AACA,eAAW,OAAO,MAAM;AACtB,YAAM,KAAK,KAAK,aAAa,GAAG;AAChC,cAAQ,eAAe,IAAI,IAAI,EAAE;AAAA,IACnC;AACA,YAAQ,YAAY,KAAK,OAAO,SAAO,IAAI,SAAS,OAAO,EAAE,IAAI,SAAO,IAAI,EAAE;AAa9E,SAAK,KAAK,KAAK,gBAAgB,GAAG,OAAO,EAAE,iBAAiB,IAAI;AAIhE,SAAK,KAAK,KAAK,gBAAgB,GAAG,OAAO,EAAE,eAAe,QAAQ,KAAK;AAAA,EACzE;AAAA;AAAA,EAGA,MAAc,uBAAsC;AAClD,UAAM,OAAoB;AAAA,MACxB,EAAE,IAAI,eAAe,MAAM,WAAW,QAAQ,EAAE,UAAM,mBAAM,gBAAgB,EAAE,EAAE;AAAA,MAChF;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,qBAAqB;AAAA,UACjC,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,UACP,MAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,qBAAqB;AAAA,UACjC,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,UACP,MAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,yBAAyB;AAAA,UACrC,UAAM,mBAAM,oBAAoB;AAAA,UAChC,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,UACP,MAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,UAKN,UAAM,mBAAM,qBAAqB;AAAA,UACjC,UAAM,mBAAM,qBAAqB;AAAA,UACjC,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,UACP,MAAM;AAAA,QACR;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,mBAAmB;AAAA,UAC/B,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,uBAAuB;AAAA,UACnC,UAAM,mBAAM,kBAAkB;AAAA,UAC9B,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ,EAAE,UAAM,mBAAM,oBAAoB,GAAG,MAAM,UAAU,MAAM,SAAS,MAAM,MAAM,OAAO,MAAM;AAAA,MACvG;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ,EAAE,UAAM,mBAAM,mBAAmB,GAAG,MAAM,UAAU,MAAM,SAAS,MAAM,MAAM,OAAO,MAAM;AAAA,MACtG;AAAA,IACF;AACA,eAAW,OAAO,MAAM;AACtB,YAAM,KAAK,KAAK,aAAa,GAAG;AAAA,IAClC;AAAA,EACF;AACF;",
|
|
4
|
+
"sourcesContent": ["import { tName } from \"./i18n\";\nimport type { AccountConfig } from \"./pure-helpers\";\nimport { PROVIDER_LABELS } from \"./sign-in\";\nimport { FetchError, type UsageProvider, type UsageSnapshot } from \"./provider\";\nimport { limitingWindow, lockedWindows, mapSnapshot, orphanObjectIds, type ObjectDef } from \"./snapshot-tree\";\nimport { computeTotals, type AccountStatus } from \"./totals\";\n\n/**\n * What the adapter currently knows about one account, in one word.\n *\n * `ok` and `rate-limited` mean the AI service is up and talking to us,\n * `unauthorized` means it is up but rejects our sign-in, `service-down` means the\n * service itself answered with a fault, `no-connection` means we never reached it.\n */\nexport type AccountState = \"ok\" | \"unauthorized\" | \"rate-limited\" | \"service-down\" | \"no-connection\" | \"not-signed-in\";\n\n/**\n * Whether an account is delivering usable numbers right now.\n *\n * ONE answer for the whole engine. The connection icon read it off the account's\n * state while the totals and `info.connection` read a second flag, so a first poll\n * that hit a throttle left the account green next to \"0 accounts reachable\".\n *\n * A throttle keeps the last values and the service is fine \u2014 that stays green; a\n * missing sign-in, a rejected one, a broken service and no connection do not.\n *\n * @param state the account's state\n * @returns true while the account counts as delivering\n */\nexport function isDelivering(state: AccountState): boolean {\n return state === \"ok\" || state === \"rate-limited\";\n}\n\n/** Consecutive network failures after which an account is judged unreachable. */\nconst MAX_NETWORK_FAILURES = 3;\n/** First backoff after a rate-limit answer (ms); doubles per repeat. */\nconst BACKOFF_START_MS = 10 * 60 * 1000;\n/** Backoff ceiling (ms). */\nconst BACKOFF_MAX_MS = 60 * 60 * 1000;\n/** Stagger between the accounts' first polls (ms) so they never fire in one burst. */\nconst STAGGER_MS = 3000;\n\n/**\n * What `info.error` says while the adapter itself has nothing to report: switched\n * off, or started and not asked yet.\n *\n * ONE wording for the whole fleet (krobi 2026-08-27) \u2014 the datapoint otherwise ends\n * up saying something different in every adapter. It stays a single word: the field\n * names the reason, it does not explain itself.\n */\nconst REASON_UNKNOWN = \"Unknown\";\n\n/** What `info.error` says for an account the adapter has no usable credential for. */\nconst NO_CREDENTIAL_REASON = \"No API key selected \u2014 pick one in the instance settings\";\n\n/** The adapter callbacks the engine drives \u2014 narrow, so tests need no adapter mock. */\nexport interface EngineDeps {\n /** Create or update an object. */\n upsertObject(def: ObjectDef): Promise<void>;\n /** Delete an object and everything below it. */\n deleteObject(id: string): Promise<void>;\n /** Every state id that currently exists below `prefix` (relative to the instance). */\n listStateIds(prefix: string): Promise<string[]>;\n /** Write a state value with ack \u2014 for MEASUREMENTS, where every cycle carries information. */\n setState(id: string, value: boolean | number | string): void;\n /**\n * Write a state only when the value differs from what the database holds \u2014 for\n * INDICATORS. js-controller does the comparison (`setStateChangedAsync`), which is\n * what the rest of the fleet uses; a hand-rolled cache would only know what this\n * process wrote and would still write blindly after a restart.\n *\n * Returns a promise so the shutdown path can WAIT for its writes; everywhere else\n * it is deliberately ignored \u2014 a poll cycle must not be held up by the database.\n */\n setStateChanged(id: string, value: boolean | number | string): Promise<void>;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n /**\n * Report whether an account's stored sign-in was REJECTED by the provider.\n *\n * Called on every state change of that fact, so the settings page can tell a\n * live sign-in from a token file that merely still exists. Without it the card\n * read \"signed in\" off file existence alone and showed a green check next to an\n * amber \"Sign-in rejected\" badge.\n *\n * @param accountId the account's object id\n * @param rejected true when the last query failed with `auth`\n */\n authState?(accountId: string, rejected: boolean): void;\n /**\n * Called once, when every account has finished its FIRST poll.\n *\n * The first round is staggered on purpose, so the adapter cannot report what the\n * object tree gained until the last account has been through. A config change\n * restarts the instance, so this is also the only moment a user needs the report.\n */\n afterFirstRound?(): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n /**\n * The account's source, or null when there is none to build \u2014 a key account\n * whose credential is missing or unreadable.\n *\n * Such an account still gets its objects and its start stamp. Skipping it\n * outright left the tree of a previously working account standing with its last\n * values AND its last status, so a key that had been removed showed as online\n * for as long as nobody looked (decision 19c applies to every account, not only\n * to the ones that can be polled).\n */\n provider: UsageProvider | null;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Whether the last query was rejected as unauthorized \u2014 reported to the sign-in card. */\n authRejected: boolean;\n /** Whether a plan-wide window was reported as CLOSED by the provider on the last answer. */\n locked: boolean;\n /** Whether the AI service itself answered on the last attempt. */\n serviceOnline: boolean;\n /** The account's one-word state. */\n state: AccountState;\n /** Plain-text reason shown in `info.error`; empty while everything works. */\n error: string;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n /** Whether this account has been through its first poll. */\n firstPollDone: boolean;\n /** True while a poll of this account is in flight \u2014 a second one must not overlap. */\n polling: boolean;\n /** Set when a poll was requested while one was running; runs once the current one ends. */\n pollAgain: boolean;\n /**\n * The dynamic state ids the last snapshot delivered, or null until the first\n * reconcile \u2014 which reads the database, so a datapoint that vanished while the\n * adapter was stopped is caught too.\n */\n deliveredIds: string[] | null;\n /** The skeleton's own state ids \u2014 they never expire. */\n staticIds: string[];\n}\n\n/**\n * Drives the polling of all configured accounts: staggered starts, one independent\n * cycle per account, typed failure handling (auth = immediate + one notification,\n * rate-limit = backoff keeping last values, service = reported down at once,\n * network = tolerated three times), warn-threshold transitions on the PLAN-WIDE\n * windows only, and the adapter-wide totals. Pure orchestration \u2014 all IO is injected.\n */\nexport class PollEngine {\n private readonly runtimes: AccountRuntime[] = [];\n private readonly handles: unknown[] = [];\n private stopped = false;\n private firstRoundReported = false;\n /**\n * How many accounts the user switched on \u2014 including those the adapter cannot\n * poll because their credential is missing. `total.accounts` is what the user\n * configured, not what happened to work out.\n */\n private readonly configuredAccounts: number;\n\n /**\n * @param accounts the validated account configs\n * @param providers each account id's provider (accounts without one are skipped)\n * @param intervalSec the poll interval in seconds\n * @param deps the injected adapter callbacks\n */\n public constructor(\n accounts: readonly AccountConfig[],\n providers: ReadonlyMap<string, UsageProvider>,\n private readonly intervalSec: number,\n private readonly deps: EngineDeps,\n ) {\n this.configuredAccounts = accounts.length;\n for (const config of accounts) {\n const provider = providers.get(config.id) ?? null;\n if (!provider) {\n // No warning here: the adapter already said which credential it could not\n // read, with the reason. A second line would only repeat it.\n deps.log.debug(`${config.name}: no usable credential \u2014 the account is shown as not delivering`);\n }\n this.runtimes.push({\n config,\n provider,\n status: { reachable: false, warning: false },\n failCount: 0,\n skipUntil: 0,\n backoffMs: BACKOFF_START_MS,\n authNotified: false,\n authRejected: false,\n serviceOnline: false,\n state: provider ? \"no-connection\" : \"not-signed-in\",\n // Nothing known until the service says something; the skeleton writes\n // REASON_UNKNOWN, and the first answer replaces it. An account without a\n // credential has its answer already.\n error: provider ? REASON_UNKNOWN : NO_CREDENTIAL_REASON,\n locked: false,\n createdObjects: new Set(),\n firstPollDone: false,\n polling: false,\n pollAgain: false,\n deliveredIds: null,\n staticIds: [],\n });\n }\n }\n\n /** Create the static per-account and totals objects, then arm the poll cycles. */\n public async start(): Promise<void> {\n for (const runtime of this.runtimes) {\n await this.createAccountSkeleton(runtime);\n }\n await this.createTotalsSkeleton();\n this.writeTotals();\n if (this.runtimes.length === 0) {\n // Nothing will ever poll \u2014 report right away, the cleanup may still have changed something.\n this.reportFirstRoundOnce();\n return;\n }\n this.runtimes.forEach((runtime, index) => {\n if (!runtime.provider) {\n // Nothing to ask. The skeleton above has already said so; count it as\n // been-through so it cannot hold the first-round report back for ever.\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n return;\n }\n // The repeating timer is armed INSIDE the staggered first poll, not next to\n // it: armed here it would start counting for every account in the same\n // instant, and from the second round on all of them would fire together \u2014\n // exactly the burst the stagger exists to prevent, against providers that\n // answer a burst by locking the whole account for a day.\n this.handles.push(\n this.deps.scheduleOnce(() => {\n if (this.stopped) {\n return;\n }\n this.handles.push(this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000));\n void this.pollAccount(runtime);\n }, index * STAGGER_MS),\n );\n });\n }\n\n /** Cancel every timer. Synchronous \u2014 safe from onUnload. */\n public stop(): void {\n this.stopped = true;\n for (const handle of this.handles) {\n this.deps.cancel(handle);\n }\n this.handles.length = 0;\n }\n\n /**\n * Say that no account is delivering any more \u2014 for shutdown.\n *\n * A stopped adapter reads nothing, so it must not leave every account claiming to\n * be online: `info.unreach` is what colours the account in the admin's object tree\n * and the badge in the settings, and on its last value it stays green for as long\n * as the instance is switched off.\n *\n * `info.error` goes back to {@link REASON_UNKNOWN}: while the instance is off the\n * adapter has nothing to report, and the fleet-wide word for that is the single\n * word \"Unknown\" (krobi 2026-08-27) \u2014 never a stale provider message, never an\n * invented sentence about our own operating state.\n *\n * The returned promise is what makes this WORK. Measured on the live server\n * 2026-08-27: issued fire-and-forget and followed by an immediate `callback()`,\n * not one of these writes ever reached the database \u2014 the process was gone first.\n * The caller has to wait for this before telling the host it is done \u2014 the host's\n * own deadline is the only limit there is.\n *\n * @returns resolves once every write has been acknowledged\n */\n public async markAllOffline(): Promise<void> {\n const writes = this.runtimes.flatMap(runtime => [\n this.deps.setStateChanged(`${runtime.config.id}.info.unreach`, true),\n this.deps.setStateChanged(`${runtime.config.id}.info.error`, REASON_UNKNOWN),\n ]);\n // The same lie one level up: \"accounts currently delivering data\" is zero.\n writes.push(this.deps.setStateChanged(\"total.accountsReachable\", 0));\n writes.push(this.deps.setStateChanged(\"info.connection\", false));\n await Promise.all(writes);\n }\n\n /**\n * Poll one account immediately, by id. Used after a successful sign-in: waiting\n * up to a full interval there reads as \"the sign-in did not work\".\n *\n * @param accountId the account's object id\n */\n public async pollNow(accountId: string): Promise<void> {\n const runtime = this.runtimes.find(entry => entry.config.id === accountId);\n if (runtime) {\n // A fresh sign-in clears a previous auth failure and any backoff.\n runtime.authNotified = false;\n runtime.authRejected = false;\n runtime.skipUntil = 0;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * Poll one account now (also used by the staggered first run).\n *\n * Never two at once for the same account: a sign-in triggers an immediate poll,\n * which can land on top of a scheduled one \u2014 and two token refreshes in parallel\n * on a rotating refresh token sign each other out. A request that arrives while\n * one is running is remembered and runs right after, so nothing is lost.\n *\n * @param runtime the account's runtime\n */\n private async pollAccount(runtime: AccountRuntime): Promise<void> {\n if (this.stopped || !runtime.provider) {\n return;\n }\n if (runtime.polling) {\n runtime.pollAgain = true;\n return;\n }\n runtime.polling = true;\n try {\n await this.pollOnce(runtime);\n } finally {\n runtime.polling = false;\n }\n if (runtime.pollAgain && !this.stopped) {\n runtime.pollAgain = false;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * One poll of one account: fetch, classify, write.\n *\n * @param runtime the account's runtime\n */\n private async pollOnce(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n if (this.deps.now() < runtime.skipUntil) {\n this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);\n // Still counts as \"been through\": otherwise an account that starts inside a\n // backoff would hold the first-round report back forever.\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n return;\n }\n const wasFailing = runtime.state !== \"ok\";\n try {\n const snapshot = await (runtime.provider as UsageProvider).fetch();\n // The answer can arrive AFTER onUnload: `stop()` cancels the timers, it\n // cannot cancel a request already in flight. Writing now would undo\n // `markAllOffline()` and leave a stopped adapter claiming the account is\n // online \u2014 the exact state decision 19 exists to prevent.\n if (this.stopped) {\n return;\n }\n runtime.failCount = 0;\n runtime.backoffMs = BACKOFF_START_MS;\n runtime.authNotified = false;\n runtime.status.snapshot = snapshot;\n runtime.serviceOnline = true;\n runtime.state = \"ok\";\n runtime.error = \"\";\n if (runtime.authRejected) {\n runtime.authRejected = false;\n this.deps.authState?.(config.id, false);\n }\n if (wasFailing) {\n // The failure said so in the log; the recovery has to as well, or the\n // last word on this account stays a warning that is no longer true.\n this.deps.log.info(`${config.name}: delivering again`);\n }\n await this.applySnapshot(runtime, snapshot);\n } catch (e) {\n if (this.stopped) {\n return;\n }\n this.handleFailure(runtime, e);\n }\n runtime.status.reachable = isDelivering(runtime.state);\n this.writeAccountInfo(runtime);\n this.writeTotals();\n runtime.firstPollDone = true;\n this.reportFirstRoundOnce();\n }\n\n /** Fire the first-round hook exactly once, when no account is still pending. */\n private reportFirstRoundOnce(): void {\n if (this.firstRoundReported || this.runtimes.some(runtime => !runtime.firstPollDone)) {\n return;\n }\n this.firstRoundReported = true;\n this.deps.afterFirstRound?.();\n }\n\n /**\n * Write a successful snapshot: upsert new objects (create-once cache), write the\n * values, and run the warn-threshold transition.\n *\n * @param runtime the account's runtime\n * @param snapshot the fetched snapshot\n */\n private async applySnapshot(runtime: AccountRuntime, snapshot: UsageSnapshot): Promise<void> {\n const { config } = runtime;\n const { objects, writes } = mapSnapshot(config.id, snapshot);\n for (const object of objects) {\n if (!runtime.createdObjects.has(object.id)) {\n await this.deps.upsertObject(object);\n runtime.createdObjects.add(object.id);\n }\n }\n for (const write of writes) {\n this.deps.setState(write.id, write.value);\n }\n await this.removeVanished(\n runtime,\n writes.map(write => write.id),\n );\n // Only PLAN-WIDE windows speak for the account \u2014 a per-model bucket at 100 %\n // must not read as \"this AI is full\" (krobi 2026-08-26, again 2026-09-06:\n // \"fable 100% ist das fable limit, aber weder das 5h stunden limit noch das\n // wochenlimit\").\n const driver = limitingWindow(snapshot);\n const percent = driver?.percent ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n // A provider that CLOSED a plan-wide window has said outright what the\n // percentage only implies. Both count.\n const locked = lockedWindows(snapshot);\n if (locked.length > 0 && !runtime.locked) {\n this.deps.log.warn(`${config.name}: ${locked[0].label} is locked by the provider \u2014 ${locked[0].reason}`);\n }\n runtime.locked = locked.length > 0;\n // Indicators go through the changed-write, measurements through the normal one.\n void this.deps.setStateChanged(`${config.id}.warning`, runtime.status.warning);\n void this.deps.setStateChanged(`${config.id}.limitReached`, percent >= 100 || runtime.locked);\n if (runtime.status.warning && !wasWarning) {\n // Always name the window: \"usage at 100 %\" without it was misleading whenever\n // several windows existed.\n const window = driver ? `${driver.label} ` : \"\";\n const message = `${config.name}: ${window}at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Delete what this account no longer delivers.\n *\n * The first round after a start compares against the DATABASE, so a window or\n * model that disappeared while the adapter was stopped is caught as well; every\n * round after that compares against the previous snapshot, which costs nothing.\n *\n * @param runtime the account's runtime\n * @param delivered the state ids this snapshot wrote\n */\n private async removeVanished(runtime: AccountRuntime, delivered: string[]): Promise<void> {\n const known = runtime.deliveredIds ?? (await this.deps.listStateIds(runtime.config.id));\n for (const id of orphanObjectIds(known, delivered, runtime.staticIds)) {\n await this.deps.deleteObject(id);\n runtime.createdObjects.delete(id);\n this.deps.log.info(`${runtime.config.name}: removed \"${id}\" \u2014 the provider no longer reports it`);\n }\n runtime.deliveredIds = delivered;\n }\n\n /**\n * Classify a fetch failure.\n *\n * The split matters for the online indicator: with `auth` and `rate-limit` the AI\n * service ANSWERED \u2014 it is online, it just said no \u2014 so only our own access is\n * broken. `service` means the service answered with a fault of its own and is\n * reported as down at once (it told us, that is not a flake). A `network` failure\n * is tolerated MAX_NETWORK_FAILURES times before we call the connection gone, so\n * a single hiccup does not make the indicator flap.\n *\n * @param runtime the account's runtime\n * @param error the thrown error\n */\n private handleFailure(runtime: AccountRuntime, error: unknown): void {\n const { config } = runtime;\n const message = error instanceof Error ? error.message : String(error);\n if (error instanceof FetchError && error.kind === \"no-credentials\") {\n // Nobody has signed in yet, or the key is gone. That is not a rejected\n // sign-in: no notification, no warning, and the settings page keeps\n // offering the sign-in button instead of an error.\n runtime.serviceOnline = true;\n runtime.state = \"not-signed-in\";\n runtime.error = message;\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n this.deps.log.info(`${config.name}: ${message}`);\n }\n return;\n }\n if (error instanceof FetchError && error.kind === \"auth\") {\n runtime.serviceOnline = true;\n runtime.state = \"unauthorized\";\n runtime.error = `Sign-in rejected \u2014 ${message}`;\n if (!runtime.authRejected) {\n runtime.authRejected = true;\n this.deps.authState?.(config.id, true);\n }\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n const text = `${config.name}: credentials rejected \u2014 ${message}`;\n this.deps.log.warn(text);\n this.deps.notify?.(config.name, text);\n }\n return;\n }\n\n if (error instanceof FetchError && error.kind === \"rate-limit\") {\n runtime.serviceOnline = true;\n runtime.state = \"rate-limited\";\n runtime.error = `Throttled by the provider \u2014 retrying in ${Math.round(runtime.backoffMs / 60000)} min, last values kept`;\n runtime.skipUntil = this.deps.now() + runtime.backoffMs;\n this.deps.log.warn(\n `${config.name}: rate-limited \u2014 backing off for ${Math.round(runtime.backoffMs / 60000)} min, keeping last values`,\n );\n runtime.backoffMs = Math.min(BACKOFF_MAX_MS, runtime.backoffMs * 2);\n return;\n }\n if (error instanceof FetchError && error.kind === \"service\") {\n runtime.failCount = 0;\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: the service reports a fault (${message}) \u2014 values kept`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"service-down\";\n runtime.error = `The AI service reports a fault \u2014 ${message}`;\n return;\n }\n runtime.failCount++;\n this.deps.log.debug(`${config.name}: fetch failed (${message}), attempt ${runtime.failCount}`);\n if (runtime.failCount >= MAX_NETWORK_FAILURES) {\n if (runtime.serviceOnline) {\n this.deps.log.warn(`${config.name}: not reachable after ${runtime.failCount} attempts (${message})`);\n }\n runtime.serviceOnline = false;\n runtime.state = \"no-connection\";\n runtime.error = `Not reachable after ${runtime.failCount} attempts \u2014 ${message}`;\n }\n }\n\n /**\n * Write one account's info states (offline marker, error text, last update).\n *\n * The stamp only moves when a poll actually brought a snapshot. It used to hang\n * on `reachable`, and that includes `rate-limited` by design \u2014 so every throttled\n * poll re-dated values it had not fetched: after a day of throttling the stamp\n * read \"an hour ago\" next to day-old numbers, which is the one thing a datapoint\n * called \"last successful update\" must never do (measured 2026-09-07 while\n * writing its description).\n *\n * @param runtime the account's runtime\n */\n private writeAccountInfo(runtime: AccountRuntime): void {\n const { config } = runtime;\n void this.writeAccountStatus(runtime);\n if (runtime.state === \"ok\") {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /**\n * Write the two status states.\n *\n * `unreach` drives the connection icon the admin draws next to the account, so it\n * has to mean what a user reads into that icon: green while the account delivers.\n * A throttle keeps the last values and the service is fine, so it stays green and\n * only fills the error text; a dead sign-in, a broken service or no connection at\n * all turn it off. Both go through the changed-write: an indicator rewritten every\n * cycle floods the history and hides the real transition.\n *\n * @param runtime the account's runtime\n */\n private async writeAccountStatus(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const delivering = isDelivering(runtime.state);\n await Promise.all([\n this.deps.setStateChanged(`${config.id}.info.unreach`, !delivering),\n this.deps.setStateChanged(`${config.id}.info.error`, runtime.error),\n ]);\n }\n\n /** Recompute and write the totals + info.connection. */\n private writeTotals(): void {\n const totals = computeTotals(\n this.runtimes.map(runtime => runtime.status),\n this.configuredAccounts,\n );\n // Every total goes through the changed-write. They are recomputed after EVERY\n // account's poll \u2014 with several accounts that meant one write per account per\n // round, almost always of the value that was already there. What they say only\n // changes when an account's numbers change, and then the write happens.\n void this.deps.setStateChanged(\"total.costs.today\", totals.costsToday);\n void this.deps.setStateChanged(\"total.costs.month\", totals.costsMonth);\n void this.deps.setStateChanged(\"total.costs.projectedMonth\", totals.costsProjectedMonth);\n void this.deps.setStateChanged(\"total.maxLimitPercent\", totals.maxLimitPercent);\n void this.deps.setStateChanged(\"total.warningsActive\", totals.warningsActive);\n void this.deps.setStateChanged(\"total.limitReached\", totals.limitReached);\n void this.deps.setStateChanged(\"total.accountsReachable\", totals.accountsReachable);\n // The configured count only ever changes with the configuration, which restarts\n // the instance \u2014 rewriting it every cycle would be pure noise in a recording.\n void this.deps.setStateChanged(\"total.accounts\", totals.accounts);\n void this.deps.setStateChanged(\"info.connection\", totals.accountsReachable > 0);\n }\n\n /**\n * The static per-account objects that exist regardless of what the source delivers.\n *\n * @param runtime the account's runtime\n */\n private async createAccountSkeleton(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const defs: ObjectDef[] = [\n {\n id: config.id,\n type: \"device\",\n common: {\n // The readable provider name, never the internal kind: the node used to\n // read \"Claude (claude-sub)\" and \"My key (anthropic-api)\" in the tree.\n //\n // The bracket stays even when it repeats the name (\"Claude (Claude)\") \u2014\n // krobi decided that on 2026-09-06 after seeing that the repetition is\n // the normal case (the settings page names a subscription row after its\n // provider, and a key account inherits the name of its credential entry).\n // The provider is then always visible, including for a freely named\n // access (\"Arbeitskonto (OpenRouter)\"). Do not propose dropping it again.\n name: `${config.name} (${PROVIDER_LABELS[config.provider] ?? config.provider})`,\n // The admin's object tree draws its connection icon from this link and\n // from nothing else \u2014 govee, beszel, homewizard and nut2 all do the same.\n statusStates: { offlineId: \"info.unreach\" },\n },\n },\n { id: `${config.id}.info`, type: \"channel\", common: { name: tName(\"nameAccountInfo\") } },\n {\n // The two slots ioBroker itself provides for this \u2014 measured against\n // @iobroker/type-detector 6.0.0: `unreach` is the offline marker every\n // device type carries (`indicator.reachable` is deprecated there), and\n // `indicator.error` is the standard home for the reason.\n id: `${config.id}.info.unreach`,\n type: \"state\",\n common: {\n name: tName(\"nameUnreach\"),\n desc: tName(\"descUnreach\"),\n type: \"boolean\",\n role: \"indicator.maintenance.unreach\",\n read: true,\n write: false,\n },\n },\n {\n // NOT `indicator.error`: the two official sources disagree \u2014 the type-detector\n // lists that role as a String, the repochecker's validity whitelist allows\n // boolean only (E1009). Validity wins, so the message rides on `text`.\n id: `${config.id}.info.error`,\n type: \"state\",\n common: {\n name: tName(\"nameLastError\"),\n desc: tName(\"descLastError\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n },\n },\n {\n id: `${config.id}.info.lastUpdate`,\n type: \"state\",\n common: {\n name: tName(\"nameLastUpdate\"),\n desc: tName(\"descLastUpdate\"),\n type: \"string\",\n role: \"date\",\n read: true,\n write: false,\n },\n },\n {\n id: `${config.id}.warning`,\n type: \"state\",\n common: {\n name: tName(\"nameWarning\"),\n desc: tName(\"descWarning\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n {\n id: `${config.id}.limitReached`,\n type: \"state\",\n common: {\n name: tName(\"nameLimitReached\"),\n desc: tName(\"descLimitReached\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n runtime.createdObjects.add(def.id);\n }\n runtime.staticIds = defs.filter(def => def.type === \"state\").map(def => def.id);\n // Mark it as not delivering right away, before anything has been asked.\n //\n // This looks pessimistic and is the honest state: nothing has been read yet. It\n // also carries the whole weight of \"the instance was off\". Whatever the previous\n // run left behind stands until someone overwrites it \u2014 after a hard kill, after\n // a crash, after an unclean shutdown the account would otherwise sit there green\n // and claim to deliver while no process exists at all. nut2 marks its devices\n // unreachable on start for exactly this reason.\n //\n // The window is short: the first poll follows within seconds and writes the real\n // state, success or failure. Only the marker \u2014 `info.error` belongs to the AI\n // service, and \"we have not asked yet\" is not something it said.\n void this.deps.setStateChanged(`${config.id}.info.unreach`, true);\n // REASON_UNKNOWN while nothing has been asked yet \u2014 but an account without a\n // usable credential has its answer already, and repeating \"Unknown\" there\n // would hide it.\n void this.deps.setStateChanged(`${config.id}.info.error`, runtime.error);\n }\n\n /** The totals skeleton (channel + states). */\n private async createTotalsSkeleton(): Promise<void> {\n const defs: ObjectDef[] = [\n { id: \"total.costs\", type: \"channel\", common: { name: tName(\"nameTotalCosts\") } },\n {\n id: \"total.costs.today\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalCostsToday\"),\n desc: tName(\"descTotalCosts\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.month\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalCostsMonth\"),\n desc: tName(\"descTotalCosts\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.projectedMonth\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalCostsProjected\"),\n desc: tName(\"descTotalCostsProjected\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.maxLimitPercent\",\n type: \"state\",\n common: {\n // NOT \"plan-wide utilisation\": the value is whatever speaks for an\n // account (fullest plan-wide window OR its granted budget, see\n // limitingWindow) \u2014 the old name promised something narrower than the\n // number ever was.\n name: tName(\"nameTotalMaxPercent\"),\n desc: tName(\"descTotalMaxPercent\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"%\",\n },\n },\n {\n id: \"total.warningsActive\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalWarnings\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.limitReached\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalLimitReached\"),\n desc: tName(\"descLimitReached\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.accountsReachable\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalReachable\"),\n desc: tName(\"descTotalReachable\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.accounts\",\n type: \"state\",\n common: {\n name: tName(\"nameTotalAccounts\"),\n desc: tName(\"descTotalAccounts\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -131,11 +131,29 @@ function mapSnapshot(accountId, snapshot) {
|
|
|
131
131
|
add(state(`${accountId}.credits.percent`, (0, import_i18n.tName)("nameCreditsPercent"), "number", "value", credits.percent, "%"));
|
|
132
132
|
}
|
|
133
133
|
if (credits.granted !== void 0) {
|
|
134
|
-
add(
|
|
134
|
+
add(
|
|
135
|
+
state(
|
|
136
|
+
`${accountId}.credits.granted`,
|
|
137
|
+
(0, import_i18n.tName)("nameCreditsGranted"),
|
|
138
|
+
"number",
|
|
139
|
+
"value",
|
|
140
|
+
credits.granted,
|
|
141
|
+
unit,
|
|
142
|
+
(0, import_i18n.tName)("descCreditsGranted")
|
|
143
|
+
)
|
|
144
|
+
);
|
|
135
145
|
}
|
|
136
146
|
if (credits.toppedUp !== void 0) {
|
|
137
147
|
add(
|
|
138
|
-
state(
|
|
148
|
+
state(
|
|
149
|
+
`${accountId}.credits.toppedUp`,
|
|
150
|
+
(0, import_i18n.tName)("nameCreditsToppedUp"),
|
|
151
|
+
"number",
|
|
152
|
+
"value",
|
|
153
|
+
credits.toppedUp,
|
|
154
|
+
unit,
|
|
155
|
+
(0, import_i18n.tName)("descCreditsToppedUp")
|
|
156
|
+
)
|
|
139
157
|
);
|
|
140
158
|
}
|
|
141
159
|
if (credits.resetCredits !== void 0) {
|
|
@@ -156,7 +174,9 @@ function mapSnapshot(accountId, snapshot) {
|
|
|
156
174
|
(0, import_i18n.tName)("nameResetCreditsExpiry"),
|
|
157
175
|
"string",
|
|
158
176
|
"date",
|
|
159
|
-
windowEnd(credits.resetCreditsNextExpiry)
|
|
177
|
+
windowEnd(credits.resetCreditsNextExpiry),
|
|
178
|
+
void 0,
|
|
179
|
+
(0, import_i18n.tName)("descResetCreditsExpiry")
|
|
160
180
|
)
|
|
161
181
|
);
|
|
162
182
|
}
|
|
@@ -165,10 +185,30 @@ function mapSnapshot(accountId, snapshot) {
|
|
|
165
185
|
if (costs) {
|
|
166
186
|
channel(`${accountId}.costs`, (0, import_i18n.tName)("nameCosts"));
|
|
167
187
|
if (costs.today !== void 0) {
|
|
168
|
-
add(
|
|
188
|
+
add(
|
|
189
|
+
state(
|
|
190
|
+
`${accountId}.costs.today`,
|
|
191
|
+
(0, import_i18n.tName)("nameCostsToday"),
|
|
192
|
+
"number",
|
|
193
|
+
"value",
|
|
194
|
+
costs.today,
|
|
195
|
+
costs.currency,
|
|
196
|
+
(0, import_i18n.tName)("descCostsToday")
|
|
197
|
+
)
|
|
198
|
+
);
|
|
169
199
|
}
|
|
170
200
|
if (costs.month !== void 0) {
|
|
171
|
-
add(
|
|
201
|
+
add(
|
|
202
|
+
state(
|
|
203
|
+
`${accountId}.costs.month`,
|
|
204
|
+
(0, import_i18n.tName)("nameCostsMonth"),
|
|
205
|
+
"number",
|
|
206
|
+
"value",
|
|
207
|
+
costs.month,
|
|
208
|
+
costs.currency,
|
|
209
|
+
(0, import_i18n.tName)("descCostsMonth")
|
|
210
|
+
)
|
|
211
|
+
);
|
|
172
212
|
}
|
|
173
213
|
if (costs.total !== void 0) {
|
|
174
214
|
add(state(`${accountId}.costs.total`, (0, import_i18n.tName)("nameCostsTotal"), "number", "value", costs.total, costs.currency));
|
|
@@ -191,10 +231,30 @@ function mapSnapshot(accountId, snapshot) {
|
|
|
191
231
|
if (tokens) {
|
|
192
232
|
channel(`${accountId}.tokens`, (0, import_i18n.tName)("nameTokens"));
|
|
193
233
|
if (tokens.inputToday !== void 0) {
|
|
194
|
-
add(
|
|
234
|
+
add(
|
|
235
|
+
state(
|
|
236
|
+
`${accountId}.tokens.inputToday`,
|
|
237
|
+
(0, import_i18n.tName)("nameTokensInput"),
|
|
238
|
+
"number",
|
|
239
|
+
"value",
|
|
240
|
+
tokens.inputToday,
|
|
241
|
+
void 0,
|
|
242
|
+
(0, import_i18n.tName)("descTokensToday")
|
|
243
|
+
)
|
|
244
|
+
);
|
|
195
245
|
}
|
|
196
246
|
if (tokens.outputToday !== void 0) {
|
|
197
|
-
add(
|
|
247
|
+
add(
|
|
248
|
+
state(
|
|
249
|
+
`${accountId}.tokens.outputToday`,
|
|
250
|
+
(0, import_i18n.tName)("nameTokensOutput"),
|
|
251
|
+
"number",
|
|
252
|
+
"value",
|
|
253
|
+
tokens.outputToday,
|
|
254
|
+
void 0,
|
|
255
|
+
(0, import_i18n.tName)("descTokensToday")
|
|
256
|
+
)
|
|
257
|
+
);
|
|
198
258
|
}
|
|
199
259
|
if (tokens.perModel && tokens.perModel.length > 0) {
|
|
200
260
|
channel(`${accountId}.models`, (0, import_i18n.tName)("nameModels"));
|
|
@@ -211,7 +271,9 @@ function mapSnapshot(accountId, snapshot) {
|
|
|
211
271
|
(0, import_i18n.tName)("nameModelTokens", model.model),
|
|
212
272
|
"number",
|
|
213
273
|
"value",
|
|
214
|
-
model.tokens
|
|
274
|
+
model.tokens,
|
|
275
|
+
void 0,
|
|
276
|
+
(0, import_i18n.tName)("descModelTokens")
|
|
215
277
|
)
|
|
216
278
|
);
|
|
217
279
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/snapshot-tree.ts"],
|
|
4
|
-
"sourcesContent": ["import { tName } from \"./i18n\";\nimport type { LimitWindow, UsageSnapshot } from \"./provider\";\nimport { sanitizeId } from \"./pure-helpers\";\n\n/**\n * A window end, rounded to the minute.\n *\n * Anthropic re-computes the timestamp on every request, so the same window end\n * arrives as ...59.898Z, ...00.364Z, ...59.539Z \u2014 sub-second noise that made every\n * single poll look like a change: 952 history entries in five days for seventeen\n * real windows (measured on the live server 2026-09-06). A window end is a wall\n * clock time a user reads and an automation waits for; the minute is the honest\n * precision, and it is stable.\n *\n * @param iso the provider's timestamp\n * @returns the timestamp rounded to the minute, or \"\" when it is unusable\n */\nexport function windowEnd(iso: string | undefined): string {\n if (!iso) {\n return \"\";\n }\n const ms = Date.parse(iso);\n if (!Number.isFinite(ms)) {\n return \"\";\n }\n return new Date(Math.round(ms / 60_000) * 60_000).toISOString().replace(/\\.\\d{3}Z$/, \"Z\");\n}\n\n/** An object to create: id (relative to the instance), type and common. */\nexport interface ObjectDef {\n /** Object id relative to the instance root. */\n id: string;\n /** ioBroker object type. */\n type: \"device\" | \"channel\" | \"state\";\n /** The object's common. */\n common: {\n name: ioBroker.StringOrTranslated;\n /** A short explanation \u2014 only where the name does not already say it. */\n desc?: ioBroker.StringOrTranslated;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n /**\n * Links a device to the state that says whether it is up. THIS is what puts the\n * connection icon next to the device in the admin's object tree \u2014 the object\n * browser reads `common.statusStates` and nothing else (verified in\n * adapter-react-v5 `renderLeaf.tsx`). A relative id is expanded to\n * `<device>.<id>`. `offlineId` is inverted: true means offline.\n */\n statusStates?: { onlineId?: string; offlineId?: string; errorId?: string };\n };\n}\n\n/** A state value to write (ack). */\nexport interface StateWrite {\n /** State id relative to the instance root. */\n id: string;\n /** The value. */\n value: boolean | number | string;\n}\n\n/**\n * A read-only state definition plus its value \u2014 the tree builder's working unit.\n *\n * @param id the state id\n * @param name the object name (translation object)\n * @param type the value type\n * @param role the state role\n * @param value the current value\n * @param unit optional unit\n * @param desc a short explanation, where the name does not already give one\n * @returns the def/value pair\n */\nfunction state(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: \"boolean\" | \"number\" | \"string\",\n role: string,\n value: boolean | number | string,\n unit?: string,\n desc?: ioBroker.StringOrTranslated,\n): { def: ObjectDef; write: StateWrite } {\n const common: ObjectDef[\"common\"] = { name, type, role, read: true, write: false };\n if (unit !== undefined) {\n common.unit = unit;\n }\n // Only where there is something to explain. A description that repeats the name\n // is worse than none (fleet standard) \u2014 most datapoints here say what they are.\n if (desc !== undefined) {\n common.desc = desc;\n }\n return { def: { id, type: \"state\", common }, write: { id, value } };\n}\n\n/** What speaks for an account: a percentage, its label, and the window it came from. */\nexport interface LimitDriver {\n /** Utilisation in percent. */\n percent: number;\n /** English label for log lines and warning messages. */\n label: string;\n /** The window itself; absent when the granted budget won. */\n window?: LimitWindow;\n}\n\n/** The result of mapping one snapshot: objects to upsert (parents first) and values to write. */\nexport interface TreeResult {\n /** Objects in creation order (device, channels, states). */\n objects: ObjectDef[];\n /** State values to write with ack. */\n writes: StateWrite[];\n}\n\n/**\n * Map one account snapshot onto the harmonised object tree: `limits.*`, `credits.*`,\n * `costs.*`, `tokens.*` (+ `models.*`) \u2014 only what the snapshot carries is created.\n * Creation is capability-driven, but NOT a mirror: time-stamp companions\n * (`resetAt`, `resetCreditsNextExpiry`) are a fixed part of their window/channel\n * and are written empty while nothing runs, and once a datapoint exists it stays\n * until its whole window/model goes (see {@link orphanObjectIds}).\n *\n * The account's own device object is NOT part of this \u2014 the poll engine's skeleton\n * owns it, together with the link that draws the connection icon. Building it here\n * as well would mean two definitions of one object, and the one without the link\n * would be the one that silently wins after a cache reset.\n *\n * @param accountId the id-safe account id (device node)\n * @param snapshot the fetched snapshot\n * @returns objects (parents first) and state writes\n */\nexport function mapSnapshot(accountId: string, snapshot: UsageSnapshot): TreeResult {\n const objects: ObjectDef[] = [];\n const writes: StateWrite[] = [];\n const add = (pair: { def: ObjectDef; write: StateWrite }): void => {\n objects.push(pair.def);\n writes.push(pair.write);\n };\n const channel = (id: string, name: ioBroker.StringOrTranslated): void => {\n objects.push({ id, type: \"channel\", common: { name } });\n };\n // Who says which window is in force: the provider when it marks one itself\n // (Claude's `is_active`), otherwise the window that speaks for the account \u2014\n // the same one the warning uses. Both answers mean the same thing to a reader,\n // which is the point: one datapoint, one meaning, every provider.\n const marked = (snapshot.limits ?? []).some(limit => limit.active !== undefined);\n const driver = marked ? undefined : limitingWindow(snapshot)?.window;\n const inForce = (limit: LimitWindow): boolean => (marked ? limit.active === true : limit === driver);\n\n if (snapshot.limits && snapshot.limits.length > 0) {\n channel(`${accountId}.limits`, tName(\"nameLimits\"));\n for (const limit of snapshot.limits) {\n const windowId = sanitizeId(limit.name);\n if (!windowId) {\n continue;\n }\n // The window name is a translation object; the percent/reset datapoints put it\n // into their own frame IN EVERY LANGUAGE, not the English one everywhere.\n const windowName = tName(limit.labelKey, limit.labelArg);\n channel(`${accountId}.limits.${windowId}`, windowName);\n add(\n state(\n `${accountId}.limits.${windowId}.percent`,\n tName(\"nameWindowPercent\", windowName),\n \"number\",\n \"value\",\n limit.percent,\n \"%\",\n ),\n );\n // resetAt is a FIXED part of every window, not an optional extra. Providers\n // omit the timestamp whenever no window is currently running (Anthropic\n // sends it as null right after a reset) \u2014 treating that as \"capability\n // gone\" deleted the datapoint mid-throttle and re-created it after the\n // next use (krobi, live 2026-09-01). The datapoint stays; an empty string\n // says \"no running window\", because keeping the OLD date would be a lie.\n add(\n state(\n `${accountId}.limits.${windowId}.resetAt`,\n tName(\"nameWindowResetAt\", windowName),\n \"string\",\n \"date\",\n windowEnd(limit.resetAt),\n undefined,\n tName(\"descWindowResetAt\"),\n ),\n );\n // Which window is actually in force. Same meaning for every provider: the\n // one the account runs into next.\n add(\n state(\n `${accountId}.limits.${windowId}.active`,\n tName(\"nameWindowActive\", windowName),\n \"boolean\",\n \"indicator\",\n inForce(limit),\n undefined,\n tName(\"descWindowActive\"),\n ),\n );\n }\n }\n\n const credits = snapshot.credits;\n if (credits) {\n channel(`${accountId}.credits`, tName(\"nameCredits\"));\n const unit = credits.pieces ? \"\" : credits.currency;\n if (credits.used !== undefined) {\n add(state(`${accountId}.credits.used`, tName(\"nameCreditsUsed\"), \"number\", \"value\", credits.used, unit));\n }\n if (credits.limit !== undefined) {\n add(state(`${accountId}.credits.limit`, tName(\"nameCreditsLimit\"), \"number\", \"value\", credits.limit, unit));\n }\n if (credits.remaining !== undefined) {\n add(\n state(\n `${accountId}.credits.remaining`,\n tName(\"nameCreditsRemaining\"),\n \"number\",\n \"value\",\n credits.remaining,\n unit,\n ),\n );\n }\n if (credits.percent !== undefined) {\n add(state(`${accountId}.credits.percent`, tName(\"nameCreditsPercent\"), \"number\", \"value\", credits.percent, \"%\"));\n }\n if (credits.granted !== undefined) {\n add(state(`${accountId}.credits.granted`, tName(\"nameCreditsGranted\"), \"number\", \"value\", credits.granted, unit));\n }\n if (credits.toppedUp !== undefined) {\n add(\n state(`${accountId}.credits.toppedUp`, tName(\"nameCreditsToppedUp\"), \"number\", \"value\", credits.toppedUp, unit),\n );\n }\n if (credits.resetCredits !== undefined) {\n add(\n state(\n `${accountId}.credits.resetCredits`,\n tName(\"nameResetCredits\"),\n \"number\",\n \"value\",\n credits.resetCredits,\n undefined,\n tName(\"descResetCredits\"),\n ),\n );\n // Companion timestamp \u2014 same fixed-part rule as limits.*.resetAt: always\n // present next to the count, empty while no voucher is held.\n add(\n state(\n `${accountId}.credits.resetCreditsNextExpiry`,\n tName(\"nameResetCreditsExpiry\"),\n \"string\",\n \"date\",\n windowEnd(credits.resetCreditsNextExpiry),\n ),\n );\n }\n }\n\n const costs = snapshot.costs;\n if (costs) {\n channel(`${accountId}.costs`, tName(\"nameCosts\"));\n if (costs.today !== undefined) {\n add(state(`${accountId}.costs.today`, tName(\"nameCostsToday\"), \"number\", \"value\", costs.today, costs.currency));\n }\n if (costs.month !== undefined) {\n add(state(`${accountId}.costs.month`, tName(\"nameCostsMonth\"), \"number\", \"value\", costs.month, costs.currency));\n }\n if (costs.total !== undefined) {\n add(state(`${accountId}.costs.total`, tName(\"nameCostsTotal\"), \"number\", \"value\", costs.total, costs.currency));\n }\n if (costs.projectedMonth !== undefined) {\n add(\n state(\n `${accountId}.costs.projectedMonth`,\n tName(\"nameCostsProjected\"),\n \"number\",\n \"value\",\n costs.projectedMonth,\n costs.currency,\n tName(\"descCostsProjected\"),\n ),\n );\n }\n }\n\n const tokens = snapshot.tokens;\n if (tokens) {\n channel(`${accountId}.tokens`, tName(\"nameTokens\"));\n if (tokens.inputToday !== undefined) {\n add(state(`${accountId}.tokens.inputToday`, tName(\"nameTokensInput\"), \"number\", \"value\", tokens.inputToday));\n }\n if (tokens.outputToday !== undefined) {\n add(state(`${accountId}.tokens.outputToday`, tName(\"nameTokensOutput\"), \"number\", \"value\", tokens.outputToday));\n }\n if (tokens.perModel && tokens.perModel.length > 0) {\n channel(`${accountId}.models`, tName(\"nameModels\"));\n for (const model of tokens.perModel) {\n const modelId = sanitizeId(model.model);\n if (!modelId) {\n continue;\n }\n // A translated frame around the provider's model name \u2014 the fleet standard\n // wants a translation object on EVERY object type, and the plain string\n // here was invisible to the static name gate because it is a runtime\n // value (found by the object inventory on its first run, 2026-09-06).\n channel(`${accountId}.models.${modelId}`, tName(\"nameModel\", model.model));\n if (model.tokens !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.tokensToday`,\n tName(\"nameModelTokens\", model.model),\n \"number\",\n \"value\",\n model.tokens,\n ),\n );\n }\n }\n }\n }\n\n if (snapshot.available !== undefined) {\n // Under `credits`, where it belongs: \"is there enough balance to make calls\"\n // is a statement about the balance, not about the account. It sat at the\n // account root next to `warning` and `limitReached`, which read as a third\n // account-wide alarm.\n if (!credits) {\n channel(`${accountId}.credits`, tName(\"nameCredits\"));\n }\n add(state(`${accountId}.credits.available`, tName(\"nameAvailable\"), \"boolean\", \"indicator\", snapshot.available));\n }\n\n return { objects, writes };\n}\n\n/**\n * What decides how full an account is: the fullest PLAN-WIDE limit window OR the\n * granted budget \u2014 whichever is higher.\n *\n * The budget competes ALWAYS, not only when an account has no windows: money that\n * is nearly spent stops the account just as hard as a full time window, and Claude\n * with extra usage enabled has both at once. That is deliberate \u2014 the previous\n * wording here claimed the budget only counted \"when the account has no windows\",\n * which the code never did (audit 2026-09-04). Whichever side wins gives the\n * warning its label (\"Credits\" or the window's name), so the message always says\n * what it is talking about.\n *\n * Windows marked `scoped` cover a single model and are left out as long as a\n * plan-wide window exists \u2014 a model the user never touches may sit at 100 %\n * permanently and would pin the account's warning on forever. Their datapoints\n * still exist; they just do not speak for the account.\n *\n * When an account has ONLY model windows (Google reports no plan-wide bucket at\n * all), the fullest of them speaks instead \u2014 an account whose warning could never\n * fire would be no better than one whose warning never clears. The label carries\n * the model name, so the warning says which model it came from.\n *\n * @param snapshot the snapshot\n * @returns percent plus the label that produced it, or undefined when nothing applies\n */\nexport function limitingWindow(snapshot: UsageSnapshot): LimitDriver | undefined {\n const limits = snapshot.limits ?? [];\n const planWide = limits.filter(limit => !limit.scoped);\n let best: LimitDriver | undefined;\n for (const limit of planWide.length > 0 ? planWide : limits) {\n if (!best || limit.percent > best.percent) {\n best = { percent: limit.percent, label: limit.label, window: limit };\n }\n }\n const credits = snapshot.credits?.percent;\n if (credits !== undefined && (!best || credits > best.percent)) {\n best = { percent: credits, label: \"Credits\" };\n }\n return best;\n}\n\n/**\n * Every plan-wide window the provider has CLOSED, with its reason.\n *\n * A closed window is the provider stating that this account cannot go on \u2014 the\n * fact `limitReached` is supposed to carry, which the adapter otherwise infers\n * from `percent >= 100`. Model-scoped windows are left out for the same reason\n * they never raise the warning.\n *\n * @param snapshot the snapshot\n * @returns label and reason per locked window\n */\nexport function lockedWindows(snapshot: UsageSnapshot): { label: string; reason: string }[] {\n return (snapshot.limits ?? [])\n .filter(limit => !limit.scoped && limit.lockedReason)\n .map(limit => ({ label: limit.label, reason: limit.lockedReason as string }));\n}\n\n/**\n * What has to be deleted after a snapshot no longer carries STRUCTURE it used to.\n *\n * ioBroker never removes an object on its own: a limit window that a provider stops\n * reporting, or a model that got renamed, would sit in the tree frozen on its last\n * percentage and keep saying it forever. So every round compares what the account\n * delivers now against what it had \u2014 but only STRUCTURE goes: a whole\n * `limits.<window>` or `models.<model>` subtree whose window/model the answer no\n * longer carries at all.\n *\n * A single VALUE inside a still-delivered window never counts as an orphan.\n * Providers omit optional fields depending on the account's momentary state\n * (Anthropic drops the reset timestamp while no window runs) \u2014 deleting on that\n * removed the datapoint mid-throttle and re-created it after the next use (krobi,\n * live 2026-09-01; the same class of bug as homeconnect's childLock). Datapoints\n * never come and go with the provider's mood: once created they stay until their\n * whole window/model/account goes, and time-stamp companions are actively written\n * empty instead of being dropped.\n *\n * Channels come after their states, deepest first, so a parent is never deleted\n * before its children.\n *\n * @param known every state id the account had before (relative to the instance)\n * @param current the state ids this snapshot delivers\n * @param keep the static state ids of the skeleton, which never expire\n * @returns object ids to delete, children before parents\n */\nexport function orphanObjectIds(\n known: readonly string[],\n current: readonly string[],\n keep: readonly string[],\n): string[] {\n const surviving = new Set([...current, ...keep]);\n // The subtrees whose members the answer still delivers \u2014 a state inside one of\n // them survives even when its own id was not written this round.\n const livingSubtrees = new Set<string>();\n for (const id of current) {\n const parts = id.split(\".\");\n // <account>.limits.<window>.<state> / <account>.models.<model>.<state>\n if (parts.length >= 4 && (parts[1] === \"limits\" || parts[1] === \"models\")) {\n livingSubtrees.add(parts.slice(0, 3).join(\".\"));\n }\n }\n const goneStates = known.filter(id => {\n if (surviving.has(id)) {\n return false;\n }\n const parts = id.split(\".\");\n if (parts.length >= 4 && (parts[1] === \"limits\" || parts[1] === \"models\")) {\n // Structure: gone only when the whole window/model fell out of the answer.\n return !livingSubtrees.has(parts.slice(0, 3).join(\".\"));\n }\n // Everything outside those subtrees (credits/costs/tokens values, available)\n // stays once created \u2014 a provider that stops a field mid-life leaves a frozen\n // value, which is the smaller harm than a datapoint that comes and goes.\n return false;\n });\n const emptyChannels = new Set<string>();\n const remaining = new Set([...surviving, ...known.filter(id => !goneStates.includes(id))]);\n for (const id of goneStates) {\n const parts = id.split(\".\");\n // Walk up from the state's own channel; the account node itself (one segment)\n // belongs to the skeleton and is never touched here.\n for (let depth = parts.length - 1; depth >= 2; depth--) {\n const parent = parts.slice(0, depth).join(\".\");\n if (![...remaining].some(alive => alive.startsWith(`${parent}.`))) {\n emptyChannels.add(parent);\n }\n }\n }\n return [...goneStates, ...[...emptyChannels].sort((a, b) => b.split(\".\").length - a.split(\".\").length)];\n}\n\n/**\n * How full a snapshot's account is \u2014 the value {@link limitingWindow} settled on\n * (fullest plan-wide window or granted budget), or undefined when it has neither.\n *\n * @param snapshot the snapshot\n * @returns the utilisation percent\n */\nexport function maxLimitPercent(snapshot: UsageSnapshot): number | undefined {\n return limitingWindow(snapshot)?.percent;\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { tName } from \"./i18n\";\nimport type { LimitWindow, UsageSnapshot } from \"./provider\";\nimport { sanitizeId } from \"./pure-helpers\";\n\n/**\n * A window end, rounded to the minute.\n *\n * Anthropic re-computes the timestamp on every request, so the same window end\n * arrives as ...59.898Z, ...00.364Z, ...59.539Z \u2014 sub-second noise that made every\n * single poll look like a change: 952 history entries in five days for seventeen\n * real windows (measured on the live server 2026-09-06). A window end is a wall\n * clock time a user reads and an automation waits for; the minute is the honest\n * precision, and it is stable.\n *\n * @param iso the provider's timestamp\n * @returns the timestamp rounded to the minute, or \"\" when it is unusable\n */\nexport function windowEnd(iso: string | undefined): string {\n if (!iso) {\n return \"\";\n }\n const ms = Date.parse(iso);\n if (!Number.isFinite(ms)) {\n return \"\";\n }\n return new Date(Math.round(ms / 60_000) * 60_000).toISOString().replace(/\\.\\d{3}Z$/, \"Z\");\n}\n\n/** An object to create: id (relative to the instance), type and common. */\nexport interface ObjectDef {\n /** Object id relative to the instance root. */\n id: string;\n /** ioBroker object type. */\n type: \"device\" | \"channel\" | \"state\";\n /** The object's common. */\n common: {\n name: ioBroker.StringOrTranslated;\n /** A short explanation \u2014 only where the name does not already say it. */\n desc?: ioBroker.StringOrTranslated;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n /**\n * Links a device to the state that says whether it is up. THIS is what puts the\n * connection icon next to the device in the admin's object tree \u2014 the object\n * browser reads `common.statusStates` and nothing else (verified in\n * adapter-react-v5 `renderLeaf.tsx`). A relative id is expanded to\n * `<device>.<id>`. `offlineId` is inverted: true means offline.\n */\n statusStates?: { onlineId?: string; offlineId?: string; errorId?: string };\n };\n}\n\n/** A state value to write (ack). */\nexport interface StateWrite {\n /** State id relative to the instance root. */\n id: string;\n /** The value. */\n value: boolean | number | string;\n}\n\n/**\n * A read-only state definition plus its value \u2014 the tree builder's working unit.\n *\n * @param id the state id\n * @param name the object name (translation object)\n * @param type the value type\n * @param role the state role\n * @param value the current value\n * @param unit optional unit\n * @param desc a short explanation, where the name does not already give one\n * @returns the def/value pair\n */\nfunction state(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: \"boolean\" | \"number\" | \"string\",\n role: string,\n value: boolean | number | string,\n unit?: string,\n desc?: ioBroker.StringOrTranslated,\n): { def: ObjectDef; write: StateWrite } {\n const common: ObjectDef[\"common\"] = { name, type, role, read: true, write: false };\n if (unit !== undefined) {\n common.unit = unit;\n }\n // Only where there is something to explain. A description that repeats the name\n // is worse than none (fleet standard) \u2014 most datapoints here say what they are.\n if (desc !== undefined) {\n common.desc = desc;\n }\n return { def: { id, type: \"state\", common }, write: { id, value } };\n}\n\n/** What speaks for an account: a percentage, its label, and the window it came from. */\nexport interface LimitDriver {\n /** Utilisation in percent. */\n percent: number;\n /** English label for log lines and warning messages. */\n label: string;\n /** The window itself; absent when the granted budget won. */\n window?: LimitWindow;\n}\n\n/** The result of mapping one snapshot: objects to upsert (parents first) and values to write. */\nexport interface TreeResult {\n /** Objects in creation order (device, channels, states). */\n objects: ObjectDef[];\n /** State values to write with ack. */\n writes: StateWrite[];\n}\n\n/**\n * Map one account snapshot onto the harmonised object tree: `limits.*`, `credits.*`,\n * `costs.*`, `tokens.*` (+ `models.*`) \u2014 only what the snapshot carries is created.\n * Creation is capability-driven, but NOT a mirror: time-stamp companions\n * (`resetAt`, `resetCreditsNextExpiry`) are a fixed part of their window/channel\n * and are written empty while nothing runs, and once a datapoint exists it stays\n * until its whole window/model goes (see {@link orphanObjectIds}).\n *\n * The account's own device object is NOT part of this \u2014 the poll engine's skeleton\n * owns it, together with the link that draws the connection icon. Building it here\n * as well would mean two definitions of one object, and the one without the link\n * would be the one that silently wins after a cache reset.\n *\n * @param accountId the id-safe account id (device node)\n * @param snapshot the fetched snapshot\n * @returns objects (parents first) and state writes\n */\nexport function mapSnapshot(accountId: string, snapshot: UsageSnapshot): TreeResult {\n const objects: ObjectDef[] = [];\n const writes: StateWrite[] = [];\n const add = (pair: { def: ObjectDef; write: StateWrite }): void => {\n objects.push(pair.def);\n writes.push(pair.write);\n };\n const channel = (id: string, name: ioBroker.StringOrTranslated): void => {\n objects.push({ id, type: \"channel\", common: { name } });\n };\n // Who says which window is in force: the provider when it marks one itself\n // (Claude's `is_active`), otherwise the window that speaks for the account \u2014\n // the same one the warning uses. Both answers mean the same thing to a reader,\n // which is the point: one datapoint, one meaning, every provider.\n const marked = (snapshot.limits ?? []).some(limit => limit.active !== undefined);\n const driver = marked ? undefined : limitingWindow(snapshot)?.window;\n const inForce = (limit: LimitWindow): boolean => (marked ? limit.active === true : limit === driver);\n\n if (snapshot.limits && snapshot.limits.length > 0) {\n channel(`${accountId}.limits`, tName(\"nameLimits\"));\n for (const limit of snapshot.limits) {\n const windowId = sanitizeId(limit.name);\n if (!windowId) {\n continue;\n }\n // The window name is a translation object; the percent/reset datapoints put it\n // into their own frame IN EVERY LANGUAGE, not the English one everywhere.\n const windowName = tName(limit.labelKey, limit.labelArg);\n channel(`${accountId}.limits.${windowId}`, windowName);\n add(\n state(\n `${accountId}.limits.${windowId}.percent`,\n tName(\"nameWindowPercent\", windowName),\n \"number\",\n \"value\",\n limit.percent,\n \"%\",\n ),\n );\n // resetAt is a FIXED part of every window, not an optional extra. Providers\n // omit the timestamp whenever no window is currently running (Anthropic\n // sends it as null right after a reset) \u2014 treating that as \"capability\n // gone\" deleted the datapoint mid-throttle and re-created it after the\n // next use (krobi, live 2026-09-01). The datapoint stays; an empty string\n // says \"no running window\", because keeping the OLD date would be a lie.\n add(\n state(\n `${accountId}.limits.${windowId}.resetAt`,\n tName(\"nameWindowResetAt\", windowName),\n \"string\",\n \"date\",\n windowEnd(limit.resetAt),\n undefined,\n tName(\"descWindowResetAt\"),\n ),\n );\n // Which window is actually in force. Same meaning for every provider: the\n // one the account runs into next.\n add(\n state(\n `${accountId}.limits.${windowId}.active`,\n tName(\"nameWindowActive\", windowName),\n \"boolean\",\n \"indicator\",\n inForce(limit),\n undefined,\n tName(\"descWindowActive\"),\n ),\n );\n }\n }\n\n const credits = snapshot.credits;\n if (credits) {\n channel(`${accountId}.credits`, tName(\"nameCredits\"));\n const unit = credits.pieces ? \"\" : credits.currency;\n if (credits.used !== undefined) {\n add(state(`${accountId}.credits.used`, tName(\"nameCreditsUsed\"), \"number\", \"value\", credits.used, unit));\n }\n if (credits.limit !== undefined) {\n add(state(`${accountId}.credits.limit`, tName(\"nameCreditsLimit\"), \"number\", \"value\", credits.limit, unit));\n }\n if (credits.remaining !== undefined) {\n add(\n state(\n `${accountId}.credits.remaining`,\n tName(\"nameCreditsRemaining\"),\n \"number\",\n \"value\",\n credits.remaining,\n unit,\n ),\n );\n }\n if (credits.percent !== undefined) {\n add(state(`${accountId}.credits.percent`, tName(\"nameCreditsPercent\"), \"number\", \"value\", credits.percent, \"%\"));\n }\n if (credits.granted !== undefined) {\n add(\n state(\n `${accountId}.credits.granted`,\n tName(\"nameCreditsGranted\"),\n \"number\",\n \"value\",\n credits.granted,\n unit,\n tName(\"descCreditsGranted\"),\n ),\n );\n }\n if (credits.toppedUp !== undefined) {\n add(\n state(\n `${accountId}.credits.toppedUp`,\n tName(\"nameCreditsToppedUp\"),\n \"number\",\n \"value\",\n credits.toppedUp,\n unit,\n tName(\"descCreditsToppedUp\"),\n ),\n );\n }\n if (credits.resetCredits !== undefined) {\n add(\n state(\n `${accountId}.credits.resetCredits`,\n tName(\"nameResetCredits\"),\n \"number\",\n \"value\",\n credits.resetCredits,\n undefined,\n tName(\"descResetCredits\"),\n ),\n );\n // Companion timestamp \u2014 same fixed-part rule as limits.*.resetAt: always\n // present next to the count, empty while no voucher is held.\n add(\n state(\n `${accountId}.credits.resetCreditsNextExpiry`,\n tName(\"nameResetCreditsExpiry\"),\n \"string\",\n \"date\",\n windowEnd(credits.resetCreditsNextExpiry),\n undefined,\n tName(\"descResetCreditsExpiry\"),\n ),\n );\n }\n }\n\n const costs = snapshot.costs;\n if (costs) {\n channel(`${accountId}.costs`, tName(\"nameCosts\"));\n if (costs.today !== undefined) {\n add(\n state(\n `${accountId}.costs.today`,\n tName(\"nameCostsToday\"),\n \"number\",\n \"value\",\n costs.today,\n costs.currency,\n tName(\"descCostsToday\"),\n ),\n );\n }\n if (costs.month !== undefined) {\n add(\n state(\n `${accountId}.costs.month`,\n tName(\"nameCostsMonth\"),\n \"number\",\n \"value\",\n costs.month,\n costs.currency,\n tName(\"descCostsMonth\"),\n ),\n );\n }\n if (costs.total !== undefined) {\n add(state(`${accountId}.costs.total`, tName(\"nameCostsTotal\"), \"number\", \"value\", costs.total, costs.currency));\n }\n if (costs.projectedMonth !== undefined) {\n add(\n state(\n `${accountId}.costs.projectedMonth`,\n tName(\"nameCostsProjected\"),\n \"number\",\n \"value\",\n costs.projectedMonth,\n costs.currency,\n tName(\"descCostsProjected\"),\n ),\n );\n }\n }\n\n const tokens = snapshot.tokens;\n if (tokens) {\n channel(`${accountId}.tokens`, tName(\"nameTokens\"));\n if (tokens.inputToday !== undefined) {\n add(\n state(\n `${accountId}.tokens.inputToday`,\n tName(\"nameTokensInput\"),\n \"number\",\n \"value\",\n tokens.inputToday,\n undefined,\n tName(\"descTokensToday\"),\n ),\n );\n }\n if (tokens.outputToday !== undefined) {\n add(\n state(\n `${accountId}.tokens.outputToday`,\n tName(\"nameTokensOutput\"),\n \"number\",\n \"value\",\n tokens.outputToday,\n undefined,\n tName(\"descTokensToday\"),\n ),\n );\n }\n if (tokens.perModel && tokens.perModel.length > 0) {\n channel(`${accountId}.models`, tName(\"nameModels\"));\n for (const model of tokens.perModel) {\n const modelId = sanitizeId(model.model);\n if (!modelId) {\n continue;\n }\n // A translated frame around the provider's model name \u2014 the fleet standard\n // wants a translation object on EVERY object type, and the plain string\n // here was invisible to the static name gate because it is a runtime\n // value (found by the object inventory on its first run, 2026-09-06).\n channel(`${accountId}.models.${modelId}`, tName(\"nameModel\", model.model));\n if (model.tokens !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.tokensToday`,\n tName(\"nameModelTokens\", model.model),\n \"number\",\n \"value\",\n model.tokens,\n undefined,\n tName(\"descModelTokens\"),\n ),\n );\n }\n }\n }\n }\n\n if (snapshot.available !== undefined) {\n // Under `credits`, where it belongs: \"is there enough balance to make calls\"\n // is a statement about the balance, not about the account. It sat at the\n // account root next to `warning` and `limitReached`, which read as a third\n // account-wide alarm.\n if (!credits) {\n channel(`${accountId}.credits`, tName(\"nameCredits\"));\n }\n add(state(`${accountId}.credits.available`, tName(\"nameAvailable\"), \"boolean\", \"indicator\", snapshot.available));\n }\n\n return { objects, writes };\n}\n\n/**\n * What decides how full an account is: the fullest PLAN-WIDE limit window OR the\n * granted budget \u2014 whichever is higher.\n *\n * The budget competes ALWAYS, not only when an account has no windows: money that\n * is nearly spent stops the account just as hard as a full time window, and Claude\n * with extra usage enabled has both at once. That is deliberate \u2014 the previous\n * wording here claimed the budget only counted \"when the account has no windows\",\n * which the code never did (audit 2026-09-04). Whichever side wins gives the\n * warning its label (\"Credits\" or the window's name), so the message always says\n * what it is talking about.\n *\n * Windows marked `scoped` cover a single model and are left out as long as a\n * plan-wide window exists \u2014 a model the user never touches may sit at 100 %\n * permanently and would pin the account's warning on forever. Their datapoints\n * still exist; they just do not speak for the account.\n *\n * When an account has ONLY model windows (Google reports no plan-wide bucket at\n * all), the fullest of them speaks instead \u2014 an account whose warning could never\n * fire would be no better than one whose warning never clears. The label carries\n * the model name, so the warning says which model it came from.\n *\n * @param snapshot the snapshot\n * @returns percent plus the label that produced it, or undefined when nothing applies\n */\nexport function limitingWindow(snapshot: UsageSnapshot): LimitDriver | undefined {\n const limits = snapshot.limits ?? [];\n const planWide = limits.filter(limit => !limit.scoped);\n let best: LimitDriver | undefined;\n for (const limit of planWide.length > 0 ? planWide : limits) {\n if (!best || limit.percent > best.percent) {\n best = { percent: limit.percent, label: limit.label, window: limit };\n }\n }\n const credits = snapshot.credits?.percent;\n if (credits !== undefined && (!best || credits > best.percent)) {\n best = { percent: credits, label: \"Credits\" };\n }\n return best;\n}\n\n/**\n * Every plan-wide window the provider has CLOSED, with its reason.\n *\n * A closed window is the provider stating that this account cannot go on \u2014 the\n * fact `limitReached` is supposed to carry, which the adapter otherwise infers\n * from `percent >= 100`. Model-scoped windows are left out for the same reason\n * they never raise the warning.\n *\n * @param snapshot the snapshot\n * @returns label and reason per locked window\n */\nexport function lockedWindows(snapshot: UsageSnapshot): { label: string; reason: string }[] {\n return (snapshot.limits ?? [])\n .filter(limit => !limit.scoped && limit.lockedReason)\n .map(limit => ({ label: limit.label, reason: limit.lockedReason as string }));\n}\n\n/**\n * What has to be deleted after a snapshot no longer carries STRUCTURE it used to.\n *\n * ioBroker never removes an object on its own: a limit window that a provider stops\n * reporting, or a model that got renamed, would sit in the tree frozen on its last\n * percentage and keep saying it forever. So every round compares what the account\n * delivers now against what it had \u2014 but only STRUCTURE goes: a whole\n * `limits.<window>` or `models.<model>` subtree whose window/model the answer no\n * longer carries at all.\n *\n * A single VALUE inside a still-delivered window never counts as an orphan.\n * Providers omit optional fields depending on the account's momentary state\n * (Anthropic drops the reset timestamp while no window runs) \u2014 deleting on that\n * removed the datapoint mid-throttle and re-created it after the next use (krobi,\n * live 2026-09-01; the same class of bug as homeconnect's childLock). Datapoints\n * never come and go with the provider's mood: once created they stay until their\n * whole window/model/account goes, and time-stamp companions are actively written\n * empty instead of being dropped.\n *\n * Channels come after their states, deepest first, so a parent is never deleted\n * before its children.\n *\n * @param known every state id the account had before (relative to the instance)\n * @param current the state ids this snapshot delivers\n * @param keep the static state ids of the skeleton, which never expire\n * @returns object ids to delete, children before parents\n */\nexport function orphanObjectIds(\n known: readonly string[],\n current: readonly string[],\n keep: readonly string[],\n): string[] {\n const surviving = new Set([...current, ...keep]);\n // The subtrees whose members the answer still delivers \u2014 a state inside one of\n // them survives even when its own id was not written this round.\n const livingSubtrees = new Set<string>();\n for (const id of current) {\n const parts = id.split(\".\");\n // <account>.limits.<window>.<state> / <account>.models.<model>.<state>\n if (parts.length >= 4 && (parts[1] === \"limits\" || parts[1] === \"models\")) {\n livingSubtrees.add(parts.slice(0, 3).join(\".\"));\n }\n }\n const goneStates = known.filter(id => {\n if (surviving.has(id)) {\n return false;\n }\n const parts = id.split(\".\");\n if (parts.length >= 4 && (parts[1] === \"limits\" || parts[1] === \"models\")) {\n // Structure: gone only when the whole window/model fell out of the answer.\n return !livingSubtrees.has(parts.slice(0, 3).join(\".\"));\n }\n // Everything outside those subtrees (credits/costs/tokens values, available)\n // stays once created \u2014 a provider that stops a field mid-life leaves a frozen\n // value, which is the smaller harm than a datapoint that comes and goes.\n return false;\n });\n const emptyChannels = new Set<string>();\n const remaining = new Set([...surviving, ...known.filter(id => !goneStates.includes(id))]);\n for (const id of goneStates) {\n const parts = id.split(\".\");\n // Walk up from the state's own channel; the account node itself (one segment)\n // belongs to the skeleton and is never touched here.\n for (let depth = parts.length - 1; depth >= 2; depth--) {\n const parent = parts.slice(0, depth).join(\".\");\n if (![...remaining].some(alive => alive.startsWith(`${parent}.`))) {\n emptyChannels.add(parent);\n }\n }\n }\n return [...goneStates, ...[...emptyChannels].sort((a, b) => b.split(\".\").length - a.split(\".\").length)];\n}\n\n/**\n * How full a snapshot's account is \u2014 the value {@link limitingWindow} settled on\n * (fullest plan-wide window or granted budget), or undefined when it has neither.\n *\n * @param snapshot the snapshot\n * @returns the utilisation percent\n */\nexport function maxLimitPercent(snapshot: UsageSnapshot): number | undefined {\n return limitingWindow(snapshot)?.percent;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAsB;AAEtB,0BAA2B;AAepB,SAAS,UAAU,KAAiC;AACzD,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AACA,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,EAAE,GAAG;AACxB,WAAO;AAAA,EACT;AACA,SAAO,IAAI,KAAK,KAAK,MAAM,KAAK,GAAM,IAAI,GAAM,EAAE,YAAY,EAAE,QAAQ,aAAa,GAAG;AAC1F;AAmDA,SAAS,MACP,IACA,MACA,MACA,MACA,OACA,MACA,MACuC;AACvC,QAAM,SAA8B,EAAE,MAAM,MAAM,MAAM,MAAM,MAAM,OAAO,MAAM;AACjF,MAAI,SAAS,QAAW;AACtB,WAAO,OAAO;AAAA,EAChB;AAGA,MAAI,SAAS,QAAW;AACtB,WAAO,OAAO;AAAA,EAChB;AACA,SAAO,EAAE,KAAK,EAAE,IAAI,MAAM,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,MAAM,EAAE;AACpE;AAqCO,SAAS,YAAY,WAAmB,UAAqC;AArIpF;AAsIE,QAAM,UAAuB,CAAC;AAC9B,QAAM,SAAuB,CAAC;AAC9B,QAAM,MAAM,CAAC,SAAsD;AACjE,YAAQ,KAAK,KAAK,GAAG;AACrB,WAAO,KAAK,KAAK,KAAK;AAAA,EACxB;AACA,QAAM,UAAU,CAAC,IAAY,SAA4C;AACvE,YAAQ,KAAK,EAAE,IAAI,MAAM,WAAW,QAAQ,EAAE,KAAK,EAAE,CAAC;AAAA,EACxD;AAKA,QAAM,WAAU,cAAS,WAAT,YAAmB,CAAC,GAAG,KAAK,WAAS,MAAM,WAAW,MAAS;AAC/E,QAAM,SAAS,SAAS,UAAY,oBAAe,QAAQ,MAAvB,mBAA0B;AAC9D,QAAM,UAAU,CAAC,UAAiC,SAAS,MAAM,WAAW,OAAO,UAAU;AAE7F,MAAI,SAAS,UAAU,SAAS,OAAO,SAAS,GAAG;AACjD,YAAQ,GAAG,SAAS,eAAW,mBAAM,YAAY,CAAC;AAClD,eAAW,SAAS,SAAS,QAAQ;AACnC,YAAM,eAAW,gCAAW,MAAM,IAAI;AACtC,UAAI,CAAC,UAAU;AACb;AAAA,MACF;AAGA,YAAM,iBAAa,mBAAM,MAAM,UAAU,MAAM,QAAQ;AACvD,cAAQ,GAAG,SAAS,WAAW,QAAQ,IAAI,UAAU;AACrD;AAAA,QACE;AAAA,UACE,GAAG,SAAS,WAAW,QAAQ;AAAA,cAC/B,mBAAM,qBAAqB,UAAU;AAAA,UACrC;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN;AAAA,QACF;AAAA,MACF;AAOA;AAAA,QACE;AAAA,UACE,GAAG,SAAS,WAAW,QAAQ;AAAA,cAC/B,mBAAM,qBAAqB,UAAU;AAAA,UACrC;AAAA,UACA;AAAA,UACA,UAAU,MAAM,OAAO;AAAA,UACvB;AAAA,cACA,mBAAM,mBAAmB;AAAA,QAC3B;AAAA,MACF;AAGA;AAAA,QACE;AAAA,UACE,GAAG,SAAS,WAAW,QAAQ;AAAA,cAC/B,mBAAM,oBAAoB,UAAU;AAAA,UACpC;AAAA,UACA;AAAA,UACA,QAAQ,KAAK;AAAA,UACb;AAAA,cACA,mBAAM,kBAAkB;AAAA,QAC1B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,SAAS;AACzB,MAAI,SAAS;AACX,YAAQ,GAAG,SAAS,gBAAY,mBAAM,aAAa,CAAC;AACpD,UAAM,OAAO,QAAQ,SAAS,KAAK,QAAQ;AAC3C,QAAI,QAAQ,SAAS,QAAW;AAC9B,UAAI,MAAM,GAAG,SAAS,qBAAiB,mBAAM,iBAAiB,GAAG,UAAU,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,IACzG;AACA,QAAI,QAAQ,UAAU,QAAW;AAC/B,UAAI,MAAM,GAAG,SAAS,sBAAkB,mBAAM,kBAAkB,GAAG,UAAU,SAAS,QAAQ,OAAO,IAAI,CAAC;AAAA,IAC5G;AACA,QAAI,QAAQ,cAAc,QAAW;AACnC;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,sBAAsB;AAAA,UAC5B;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC,UAAI,MAAM,GAAG,SAAS,wBAAoB,mBAAM,oBAAoB,GAAG,UAAU,SAAS,QAAQ,SAAS,GAAG,CAAC;AAAA,IACjH;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,oBAAoB;AAAA,UAC1B;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,cACA,mBAAM,oBAAoB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AACA,QAAI,QAAQ,aAAa,QAAW;AAClC;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,qBAAqB;AAAA,UAC3B;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,cACA,mBAAM,qBAAqB;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AACA,QAAI,QAAQ,iBAAiB,QAAW;AACtC;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,kBAAkB;AAAA,UACxB;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,cACA,mBAAM,kBAAkB;AAAA,QAC1B;AAAA,MACF;AAGA;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,wBAAwB;AAAA,UAC9B;AAAA,UACA;AAAA,UACA,UAAU,QAAQ,sBAAsB;AAAA,UACxC;AAAA,cACA,mBAAM,wBAAwB;AAAA,QAChC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,SAAS;AACvB,MAAI,OAAO;AACT,YAAQ,GAAG,SAAS,cAAU,mBAAM,WAAW,CAAC;AAChD,QAAI,MAAM,UAAU,QAAW;AAC7B;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,gBAAgB;AAAA,UACtB;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN,MAAM;AAAA,cACN,mBAAM,gBAAgB;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AACA,QAAI,MAAM,UAAU,QAAW;AAC7B;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,gBAAgB;AAAA,UACtB;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN,MAAM;AAAA,cACN,mBAAM,gBAAgB;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AACA,QAAI,MAAM,UAAU,QAAW;AAC7B,UAAI,MAAM,GAAG,SAAS,oBAAgB,mBAAM,gBAAgB,GAAG,UAAU,SAAS,MAAM,OAAO,MAAM,QAAQ,CAAC;AAAA,IAChH;AACA,QAAI,MAAM,mBAAmB,QAAW;AACtC;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,oBAAoB;AAAA,UAC1B;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN,MAAM;AAAA,cACN,mBAAM,oBAAoB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,SAAS;AACxB,MAAI,QAAQ;AACV,YAAQ,GAAG,SAAS,eAAW,mBAAM,YAAY,CAAC;AAClD,QAAI,OAAO,eAAe,QAAW;AACnC;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,iBAAiB;AAAA,UACvB;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP;AAAA,cACA,mBAAM,iBAAiB;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AACA,QAAI,OAAO,gBAAgB,QAAW;AACpC;AAAA,QACE;AAAA,UACE,GAAG,SAAS;AAAA,cACZ,mBAAM,kBAAkB;AAAA,UACxB;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP;AAAA,cACA,mBAAM,iBAAiB;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AACA,QAAI,OAAO,YAAY,OAAO,SAAS,SAAS,GAAG;AACjD,cAAQ,GAAG,SAAS,eAAW,mBAAM,YAAY,CAAC;AAClD,iBAAW,SAAS,OAAO,UAAU;AACnC,cAAM,cAAU,gCAAW,MAAM,KAAK;AACtC,YAAI,CAAC,SAAS;AACZ;AAAA,QACF;AAKA,gBAAQ,GAAG,SAAS,WAAW,OAAO,QAAI,mBAAM,aAAa,MAAM,KAAK,CAAC;AACzE,YAAI,MAAM,WAAW,QAAW;AAC9B;AAAA,YACE;AAAA,cACE,GAAG,SAAS,WAAW,OAAO;AAAA,kBAC9B,mBAAM,mBAAmB,MAAM,KAAK;AAAA,cACpC;AAAA,cACA;AAAA,cACA,MAAM;AAAA,cACN;AAAA,kBACA,mBAAM,iBAAiB;AAAA,YACzB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,SAAS,cAAc,QAAW;AAKpC,QAAI,CAAC,SAAS;AACZ,cAAQ,GAAG,SAAS,gBAAY,mBAAM,aAAa,CAAC;AAAA,IACtD;AACA,QAAI,MAAM,GAAG,SAAS,0BAAsB,mBAAM,eAAe,GAAG,WAAW,aAAa,SAAS,SAAS,CAAC;AAAA,EACjH;AAEA,SAAO,EAAE,SAAS,OAAO;AAC3B;AA2BO,SAAS,eAAe,UAAkD;AA5ajF;AA6aE,QAAM,UAAS,cAAS,WAAT,YAAmB,CAAC;AACnC,QAAM,WAAW,OAAO,OAAO,WAAS,CAAC,MAAM,MAAM;AACrD,MAAI;AACJ,aAAW,SAAS,SAAS,SAAS,IAAI,WAAW,QAAQ;AAC3D,QAAI,CAAC,QAAQ,MAAM,UAAU,KAAK,SAAS;AACzC,aAAO,EAAE,SAAS,MAAM,SAAS,OAAO,MAAM,OAAO,QAAQ,MAAM;AAAA,IACrE;AAAA,EACF;AACA,QAAM,WAAU,cAAS,YAAT,mBAAkB;AAClC,MAAI,YAAY,WAAc,CAAC,QAAQ,UAAU,KAAK,UAAU;AAC9D,WAAO,EAAE,SAAS,SAAS,OAAO,UAAU;AAAA,EAC9C;AACA,SAAO;AACT;AAaO,SAAS,cAAc,UAA8D;AAvc5F;AAwcE,WAAQ,cAAS,WAAT,YAAmB,CAAC,GACzB,OAAO,WAAS,CAAC,MAAM,UAAU,MAAM,YAAY,EACnD,IAAI,YAAU,EAAE,OAAO,MAAM,OAAO,QAAQ,MAAM,aAAuB,EAAE;AAChF;AA6BO,SAAS,gBACd,OACA,SACA,MACU;AACV,QAAM,YAAY,oBAAI,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC;AAG/C,QAAM,iBAAiB,oBAAI,IAAY;AACvC,aAAW,MAAM,SAAS;AACxB,UAAM,QAAQ,GAAG,MAAM,GAAG;AAE1B,QAAI,MAAM,UAAU,MAAM,MAAM,CAAC,MAAM,YAAY,MAAM,CAAC,MAAM,WAAW;AACzE,qBAAe,IAAI,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,IAChD;AAAA,EACF;AACA,QAAM,aAAa,MAAM,OAAO,QAAM;AACpC,QAAI,UAAU,IAAI,EAAE,GAAG;AACrB,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,GAAG,MAAM,GAAG;AAC1B,QAAI,MAAM,UAAU,MAAM,MAAM,CAAC,MAAM,YAAY,MAAM,CAAC,MAAM,WAAW;AAEzE,aAAO,CAAC,eAAe,IAAI,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,IACxD;AAIA,WAAO;AAAA,EACT,CAAC;AACD,QAAM,gBAAgB,oBAAI,IAAY;AACtC,QAAM,YAAY,oBAAI,IAAI,CAAC,GAAG,WAAW,GAAG,MAAM,OAAO,QAAM,CAAC,WAAW,SAAS,EAAE,CAAC,CAAC,CAAC;AACzF,aAAW,MAAM,YAAY;AAC3B,UAAM,QAAQ,GAAG,MAAM,GAAG;AAG1B,aAAS,QAAQ,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS;AACtD,YAAM,SAAS,MAAM,MAAM,GAAG,KAAK,EAAE,KAAK,GAAG;AAC7C,UAAI,CAAC,CAAC,GAAG,SAAS,EAAE,KAAK,WAAS,MAAM,WAAW,GAAG,MAAM,GAAG,CAAC,GAAG;AACjE,sBAAc,IAAI,MAAM;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AACA,SAAO,CAAC,GAAG,YAAY,GAAG,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC;AACxG;AASO,SAAS,gBAAgB,UAA6C;AA7hB7E;AA8hBE,UAAO,oBAAe,QAAQ,MAAvB,mBAA0B;AACnC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "ai-usage",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.12.1": {
|
|
7
|
+
"en": "The last-update stamp no longer moves forward while the provider is only throttling, and twenty-five more datapoints explain themselves in the object tree.",
|
|
8
|
+
"de": "Der Stempel der letzten Aktualisierung rückt nicht mehr vor, solange der Anbieter nur drosselt, und fünfundzwanzig weitere Datenpunkte erklären sich im Objektbaum selbst.",
|
|
9
|
+
"ru": "Метка последнего обновления больше не сдвигается, пока провайдер лишь ограничивает запросы, а ещё двадцать пять точек данных объясняют себя в дереве объектов.",
|
|
10
|
+
"pt": "A marca da última atualização deixa de avançar enquanto o fornecedor apenas limita os pedidos, e mais vinte e cinco pontos de dados explicam-se na árvore de objetos.",
|
|
11
|
+
"nl": "Het stempel van de laatste update loopt niet meer door zolang de aanbieder alleen beperkt, en vijfentwintig extra datapunten leggen zichzelf uit in de objectboom.",
|
|
12
|
+
"fr": "L'horodatage de la dernière mise à jour n'avance plus tant que le fournisseur ne fait que limiter, et vingt-cinq points de donnée de plus s'expliquent dans l'arbre d'objets.",
|
|
13
|
+
"it": "Il timestamp dell'ultimo aggiornamento non avanza più finché il provider si limita a rallentare, e altri venticinque datapoint si spiegano da soli nell'albero degli oggetti.",
|
|
14
|
+
"es": "La marca de la última actualización ya no avanza mientras el proveedor solo limita las peticiones, y veinticinco puntos de datos más se explican en el árbol de objetos.",
|
|
15
|
+
"pl": "Znacznik ostatniej aktualizacji nie przesuwa się już, gdy dostawca jedynie ogranicza zapytania, a kolejne dwadzieścia pięć punktów danych objaśnia się w drzewie obiektów.",
|
|
16
|
+
"uk": "Позначка останнього оновлення більше не зсувається, поки провайдер лише обмежує запити, а ще двадцять п'ять точок даних пояснюють себе в дереві об'єктів.",
|
|
17
|
+
"zh-cn": "在供应商仅限流期间,最近一次更新的时间戳不再向前推进;另有二十五个数据点会在对象树中自行说明其含义。"
|
|
18
|
+
},
|
|
6
19
|
"0.12.0": {
|
|
7
20
|
"en": "An account that is not signed in yet no longer reports a rejected sign-in, every limit window shows whether it is the one in force, and reset times stop flooding a recording.",
|
|
8
21
|
"de": "Ein noch nicht angemeldetes Konto meldet keine abgelehnte Anmeldung mehr, jedes Limit-Fenster zeigt, ob es gerade das gültige ist, und Reset-Zeiten fluten keine Aufzeichnung mehr.",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Dopóki konto nie ma nic do zgłoszenia — adapter wyłączony lub uruchomiony i jeszcze nieodpytany — jako powód widnieje teraz „Unknown\" zamiast zdania o adapterze",
|
|
81
94
|
"uk": "Поки обліковий запис не має про що повідомити — адаптер вимкнено або запущено й ще не опитано — як причина тепер стоїть «Unknown» замість фрази про адаптер",
|
|
82
95
|
"zh-cn": "当某个账户暂无可报告的内容时——适配器已停止,或刚启动尚未查询——原因显示为“Unkn”,而不再是一句关于适配器的说明"
|
|
83
|
-
},
|
|
84
|
-
"0.9.0": {
|
|
85
|
-
"en": "Stopping the instance now really marks every account as offline, and an account no longer keeps claiming to deliver data after a crash",
|
|
86
|
-
"de": "Das Stoppen der Instanz kennzeichnet jetzt wirklich jedes Konto als offline, und nach einem Absturz behauptet kein Konto mehr, es liefere Daten",
|
|
87
|
-
"ru": "Остановка экземпляра теперь действительно помечает все аккаунты как офлайн, и после сбоя ни один аккаунт больше не утверждает, что передаёт данные",
|
|
88
|
-
"pt": "Parar a instância passa a marcar realmente todas as contas como offline, e após uma falha nenhuma conta continua a afirmar que fornece dados",
|
|
89
|
-
"nl": "Het stoppen van de instantie markeert nu echt elk account als offline, en na een crash beweert geen account meer dat het gegevens levert",
|
|
90
|
-
"fr": "L'arrêt de l'instance marque désormais réellement chaque compte hors ligne, et après un plantage aucun compte ne prétend plus fournir des données",
|
|
91
|
-
"it": "L'arresto dell'istanza contrassegna davvero ogni account come offline e dopo un crash nessun account continua a sostenere di fornire dati",
|
|
92
|
-
"es": "Detener la instancia ahora marca realmente todas las cuentas como sin conexión, y tras un fallo ninguna cuenta sigue afirmando que entrega datos",
|
|
93
|
-
"pl": "Zatrzymanie instancji naprawdę oznacza teraz każde konto jako offline, a po awarii żadne konto nie twierdzi już, że dostarcza dane",
|
|
94
|
-
"uk": "Зупинка екземпляра тепер справді позначає кожен обліковий запис як офлайн, а після збою жоден із них більше не стверджує, що передає дані",
|
|
95
|
-
"zh-cn": "停止实例现在会真正把每个账户标记为离线,崩溃之后账户也不再声称自己仍在提供数据"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"plugins": {
|
package/package.json
CHANGED