Promote develop → main (2026-07-25 23:31 UTC) #53
@@ -15,9 +15,12 @@ einheitlich in Monitor und Verwaltungsportal).
|
|||||||
|
|
||||||
## Was eine Aktion kostet
|
## Was eine Aktion kostet
|
||||||
|
|
||||||
Im Modus `flat` (Voreinstellung) kostet jede Aktion einen festen Satz aus
|
Im Modus `flat` (Voreinstellung) kostet jede Aktion einen festen Satz,
|
||||||
`CREDIT_TARIFF` in `src/config.py`, unabhängig davon, was sie uns tatsächlich
|
unabhängig davon, was sie uns tatsächlich verursacht hat. 1 Credit entspricht
|
||||||
verursacht hat. 1 Credit entspricht 0,20 USD.
|
0,20 USD. Die wirksamen Sätze stehen seit 25.07.2026 in der geteilten Tabelle
|
||||||
|
`billing_tariff` und sind über den Verbrauchsrechner des Verwaltungsportals
|
||||||
|
pflegbar. `CREDIT_TARIFF` in `src/config.py` befüllt die Tabelle beim ersten
|
||||||
|
Start und bleibt Rückfallebene für fehlende Schlüssel.
|
||||||
|
|
||||||
| Aktion | Credits | Herkunft des Werts |
|
| Aktion | Credits | Herkunft des Werts |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -138,8 +141,6 @@ Zur Orientierung bei einem Kontingent von 10.000 Credits und 45 Credits je Lauf.
|
|||||||
## Offene Punkte
|
## Offene Punkte
|
||||||
|
|
||||||
- E-Mail-Versand der Budget-Warnung.
|
- E-Mail-Versand der Budget-Warnung.
|
||||||
- Eingabefelder für die Stellwerte im Verwaltungsportal (heute erzeugt das
|
|
||||||
Anlegen einer Organisation dort nicht einmal eine Lizenz).
|
|
||||||
- Takt-Untergrenze und Kostenvorschau aus dem Lokal-Fork portieren (siehe oben).
|
- Takt-Untergrenze und Kostenvorschau aus dem Lokal-Fork portieren (siehe oben).
|
||||||
- Nachkaufpakete. Ohne sie ist der Deckel eine Sackgasse, mit ihnen eine
|
- Nachkaufpakete. Ohne sie ist der Deckel eine Sackgasse, mit ihnen eine
|
||||||
Umsatzquelle. Der Verkauf gehört ins Verwaltungsportal, im Monitor müsste nur
|
Umsatzquelle. Der Verkauf gehört ins Verwaltungsportal, im Monitor müsste nur
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import aiosqlite
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from config import DB_PATH, DATA_DIR
|
from config import DB_PATH, DATA_DIR, CREDIT_TARIFF
|
||||||
|
|
||||||
logger = logging.getLogger("osint.database")
|
logger = logging.getLogger("osint.database")
|
||||||
|
|
||||||
@@ -896,6 +896,30 @@ async def init_db():
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info("Migration: unlimited_budget zu Lizenzen hinzugefuegt")
|
logger.info("Migration: unlimited_budget zu Lizenzen hinzugefuegt")
|
||||||
|
|
||||||
|
# Migration: Preistabelle fuer die Credits-Saetze. Pflegbare Quelle der
|
||||||
|
# Wahrheit fuer Monitor UND Verwaltungsportal (das Portal schreibt sie
|
||||||
|
# ueber den Verbrauchsrechner). Beim ersten Start mit den wirksamen
|
||||||
|
# Konfigurationswerten befuellt, danach gewinnt die Tabelle,
|
||||||
|
# CREDIT_TARIFF bleibt Rueckfallebene fuer fehlende Schluessel.
|
||||||
|
cursor = await db.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='billing_tariff'"
|
||||||
|
)
|
||||||
|
if not await cursor.fetchone():
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE billing_tariff (
|
||||||
|
tariff_key TEXT PRIMARY KEY,
|
||||||
|
credits REAL NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
for tariff_key, credits in CREDIT_TARIFF.items():
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO billing_tariff (tariff_key, credits) VALUES (?, ?)",
|
||||||
|
(tariff_key, float(credits)),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
logger.info("Migration: billing_tariff angelegt und mit Konfigurationswerten befuellt")
|
||||||
|
|
||||||
# Migration: Token-Usage-Monatstabelle
|
# Migration: Token-Usage-Monatstabelle
|
||||||
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'")
|
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'")
|
||||||
if not await cursor.fetchone():
|
if not await cursor.fetchone():
|
||||||
|
|||||||
@@ -371,6 +371,24 @@ async def _record_usage_statistics(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_tariff(db: aiosqlite.Connection) -> dict:
|
||||||
|
"""Wirksame Credits-Saetze je Aktion.
|
||||||
|
|
||||||
|
Die Tabelle billing_tariff ist die pflegbare Quelle (das Verwaltungsportal
|
||||||
|
schreibt sie), CREDIT_TARIFF aus der Konfiguration bleibt Rueckfallebene
|
||||||
|
fuer fehlende Schluessel oder eine noch fehlende Tabelle.
|
||||||
|
"""
|
||||||
|
tariff = dict(CREDIT_TARIFF)
|
||||||
|
try:
|
||||||
|
cursor = await db.execute("SELECT tariff_key, credits FROM billing_tariff")
|
||||||
|
for row in await cursor.fetchall():
|
||||||
|
if row["credits"] is not None and row["credits"] > 0:
|
||||||
|
tariff[row["tariff_key"]] = float(row["credits"])
|
||||||
|
except Exception:
|
||||||
|
pass # Tabelle existiert noch nicht, die Konfiguration gilt
|
||||||
|
return tariff
|
||||||
|
|
||||||
|
|
||||||
async def _charge_credits(
|
async def _charge_credits(
|
||||||
db: aiosqlite.Connection,
|
db: aiosqlite.Connection,
|
||||||
tenant_id: int,
|
tenant_id: int,
|
||||||
@@ -398,7 +416,8 @@ async def _charge_credits(
|
|||||||
key = _tariff_key(source, incident_type)
|
key = _tariff_key(source, incident_type)
|
||||||
|
|
||||||
if BILLING_MODE == "flat":
|
if BILLING_MODE == "flat":
|
||||||
credits_consumed = CREDIT_TARIFF.get(key)
|
tariff = await _load_tariff(db)
|
||||||
|
credits_consumed = tariff.get(key)
|
||||||
if credits_consumed is None:
|
if credits_consumed is None:
|
||||||
# Unbekannte Quelle. Lieber auf die echte Rechnung zurueckfallen als
|
# Unbekannte Quelle. Lieber auf die echte Rechnung zurueckfallen als
|
||||||
# stillschweigend gratis abzugeben.
|
# stillschweigend gratis abzugeben.
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren