Promote develop → main (2026-07-25 22:22 UTC) #16

Zusammengeführt
IntelSight_Admin hat 41 Commits von develop nach main 2026-07-26 00:22:10 +02:00 zusammengeführt
5 geänderte Dateien mit 257 neuen und 5 gelöschten Zeilen
Nur Änderungen aus Commit c4ea949f91 werden angezeigt - Alle Commits anzeigen

Datei anzeigen

@@ -0,0 +1,67 @@
"""Migration 2026-07-25: Preistabelle billing_tariff anlegen und befüllen.
Die Tabelle ist die pflegbare Quelle der Credits-Sätze je Aktion. Der Monitor
legt sie beim Start selbst an (init_db, mit seinen Konfigurationswerten als
Erstbefüllung). Die Portal-Staging-Umgebung nutzt aber eine EIGENE Datenbank,
auf der kein Monitor läuft, deshalb braucht sie dieses Skript. Idempotent,
vorhandene Sätze werden NICHT überschrieben (INSERT OR IGNORE).
Ausführung:
DB_PATH=/home/claude-dev/AegisSight-Monitor-staging/data/osint.db python3 migrations/2026-07-25_billing_tariff.py
DB_PATH=/home/claude-dev/osint-data/osint.db python3 migrations/2026-07-25_billing_tariff.py
"""
import os
import sqlite3
import sys
# Beschlossene Verkaufssätze (23.07.2026), identisch zu den Monitor-Defaults
SEED = {
"monitor_adhoc": 45.0,
"monitor_research": 40.0,
"analysis": 12.0,
"factcheck": 12.0,
"chat": 1.0,
"enhance": 1.0,
"globe": 1.0,
}
def main(db_path: str) -> int:
if not os.path.exists(db_path):
print(f"FEHLER: DB nicht gefunden: {db_path}", file=sys.stderr)
return 1
conn = sqlite3.connect(db_path, timeout=60)
conn.execute("PRAGMA busy_timeout = 60000")
conn.execute("PRAGMA journal_mode = WAL")
print(f"Migration auf {db_path}")
conn.execute("""
CREATE TABLE IF NOT EXISTS billing_tariff (
tariff_key TEXT PRIMARY KEY,
credits REAL NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
print(" + billing_tariff angelegt (oder vorhanden)")
for key, credits in SEED.items():
cur = conn.execute(
"INSERT OR IGNORE INTO billing_tariff (tariff_key, credits) VALUES (?, ?)",
(key, credits),
)
if cur.rowcount:
print(f" + Satz {key} = {credits}")
else:
print(f" = Satz {key} war bereits gesetzt")
conn.commit()
conn.close()
print("Migration abgeschlossen.")
return 0
if __name__ == "__main__":
db_path = os.environ.get("DB_PATH", "/home/claude-dev/osint-data/osint.db")
sys.exit(main(db_path))

Datei anzeigen

@@ -12,8 +12,10 @@ from collections import defaultdict
from datetime import datetime, timedelta
import aiosqlite
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException, Request
from auth import get_current_admin
from database import get_db
from audit import log_action, get_client_ip
logger = logging.getLogger("verwaltung.pricing")
router = APIRouter(prefix="/api/pricing", tags=["Pricing"])
@@ -21,6 +23,90 @@ router = APIRouter(prefix="/api/pricing", tags=["Pricing"])
COST_BASIS_DB = os.environ.get("COST_BASIS_DB_PATH", "/home/claude-dev/osint-data/osint.db")
USD_PER_CREDIT = 0.20
# Rückfallwerte, falls die Preistabelle (noch) fehlt. Entsprechen den
# Monitor-Defaults (CREDIT_TARIFF in AegisSight-Monitor, src/config.py).
TARIFF_DEFAULTS = {
"monitor_adhoc": 45.0,
"monitor_research": 40.0,
"analysis": 12.0,
"factcheck": 12.0,
"chat": 1.0,
"enhance": 1.0,
"globe": 1.0,
}
@router.get("/tariff")
async def get_tariff(admin=Depends(get_current_admin)):
"""Wirksame Credits-Sätze aus der geteilten Preistabelle billing_tariff.
Die Tabelle ist die Quelle der Buchung im Monitor. Fehlt sie noch
(Monitor-Migration nicht gelaufen), gelten die Rückfallwerte.
"""
tariff = dict(TARIFF_DEFAULTS)
source = "defaults"
db = await get_db()
try:
try:
cursor = await db.execute("SELECT tariff_key, credits FROM billing_tariff")
rows = await cursor.fetchall()
if rows:
source = "db"
for row in rows:
tariff[row["tariff_key"]] = row["credits"]
except Exception:
logger.warning("billing_tariff fehlt noch, liefere Rückfallwerte")
finally:
await db.close()
return {"tariff": tariff, "source": source, "usd_per_credit": USD_PER_CREDIT}
@router.put("/tariff")
async def update_tariff(data: dict, request: Request, admin=Depends(get_current_admin)):
"""Setzt Credits-Sätze in billing_tariff. Ab der nächsten Buchung wirksam."""
allowed = tuple(TARIFF_DEFAULTS.keys())
updates = {}
for key, value in (data or {}).items():
if key not in allowed:
raise HTTPException(status_code=400, detail=f"Unbekannter Tarif-Schlüssel: {key}")
try:
v = float(value)
except (TypeError, ValueError):
raise HTTPException(status_code=400, detail=f"{key} muss eine Zahl sein")
if not 0.5 <= v <= 10000:
raise HTTPException(status_code=400, detail=f"{key} muss zwischen 0,5 und 10000 liegen")
updates[key] = v
if not updates:
raise HTTPException(status_code=400, detail="Keine Sätze übergeben")
db = await get_db()
try:
cursor = await db.execute("SELECT tariff_key, credits FROM billing_tariff")
before = {r["tariff_key"]: r["credits"] for r in await cursor.fetchall()}
for key, v in updates.items():
await db.execute(
"INSERT INTO billing_tariff (tariff_key, credits, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) "
"ON CONFLICT(tariff_key) DO UPDATE SET credits = excluded.credits, updated_at = CURRENT_TIMESTAMP",
(key, v),
)
await db.commit()
await log_action(
db, admin, get_client_ip(request),
action="update", resource_type="tariff", resource_id=0,
before=before, after={**before, **updates},
)
logger.info(f"Preistabelle aktualisiert: {updates}")
return {"ok": True, "updated": updates}
except HTTPException:
raise
except Exception as exc:
raise HTTPException(
status_code=500,
detail=f"Preistabelle nicht beschreibbar ({exc}). Falls sie fehlt, zuerst den Monitor deployen oder migrations/2026-07-25_billing_tariff.py fahren.",
)
finally:
await db.close()
@router.get("/cost-basis")
async def cost_basis(days: int = 30, admin=Depends(get_current_admin)):

Datei anzeigen

@@ -271,7 +271,7 @@
</div>
<p style="margin-top:10px; font-size:12px; color:var(--text-muted); display:flex; align-items:center; gap:6px;">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0;"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
<span>Zur Einordnung. Ein Live-Lauf kostet 45 Credits, das Anlegen einer Recherche 120 (drei Durchläufe), jede Vertiefung 40, Chat und Beschreibungs-Assistent je 1 Credit.
<span><span id="tariffInfoText">Zur Einordnung. Ein Live-Lauf kostet 45 Credits, das Anlegen einer Recherche 120 (drei Durchläufe), jede Vertiefung 40, Chat und Beschreibungs-Assistent je 1 Credit.</span>
<a href="#" onclick="openRechnerTab(); return false;" style="color:var(--accent);">Verbrauchsrechner öffnen</a></span>
</p>
@@ -1217,7 +1217,7 @@
</div>
<script src="/static/js/a11y.js?v=20260725a"></script>
<script src="/static/js/app.js?v=20260725d"></script>
<script src="/static/js/app.js?v=20260725e"></script>
<script src="/static/js/sources.js?v=20260522x2"></script>
<script src="/static/js/x-scraper.js?v=20260522a"></script>
<script src="/static/js/source-health.js?v=20260509l"></script>

Datei anzeigen

@@ -69,6 +69,23 @@ const ThemeManager = {
},
};
// Wirksame Credits-Sätze aus der Preistabelle, für alle Anzeigetexte
let PORTAL_TARIFF = null;
async function loadTariffInfo() {
try {
const data = await API.get("/api/pricing/tariff");
PORTAL_TARIFF = data.tariff || null;
const el = document.getElementById("tariffInfoText");
if (el && PORTAL_TARIFF) {
const t = PORTAL_TARIFF;
const n = (v) => Number(v).toLocaleString("de-DE");
el.textContent = `Zur Einordnung. Ein Live-Lauf kostet ${n(t.monitor_adhoc)} Credits, ` +
`das Anlegen einer Recherche ${n(t.monitor_research * 3)} (drei Durchläufe), ` +
`jede Vertiefung ${n(t.monitor_research)}, Chat und Beschreibungs-Assistent je ${n(t.chat)}.`;
}
} catch (e) { /* statischer Text bleibt als Rückfallebene stehen */ }
}
// Sprung zum (nachgeladenen) Verbrauchsrechner-Tab
function openRechnerTab() {
const tab = document.querySelector('.nav-tab[data-section="rechner"]');
@@ -115,6 +132,7 @@ document.addEventListener("DOMContentLoaded", () => {
setupForms();
setupTranslation();
loadDashboard();
loadTariffInfo();
loadDashboardTokenStats();
loadTranslationStatus();
loadOrgs();
@@ -1289,6 +1307,6 @@ document.addEventListener("DOMContentLoaded", () => {
// --- Verbrauchsrechner nachladen (eigenstaendiges Modul, siehe rechner.js) ---
(function () {
var s = document.createElement("script");
s.src = "/static/js/rechner.js?v=20260725a";
s.src = "/static/js/rechner.js?v=20260725b";
document.head.appendChild(s);
})();

Datei anzeigen

@@ -157,6 +157,79 @@
var empfDaten = null;
// ---------- Aktive Preise (Tabelle billing_tariff) ----------
var aktivTarif = null; // zuletzt geladene bzw. gespeicherte aktive Saetze
function renderAktivHint() {
var el = document.getElementById("vr-aktiv-hint");
if (!el) return;
if (aktivTarif) {
el.innerHTML = "<strong>Simulation.</strong> Abgerechnet wird nach der Preistabelle, aktuell <b>" +
aktivTarif.live + "</b> je Live-Lauf, <b>" + aktivTarif.research + "</b> je Recherche-Durchlauf, <b>" +
aktivTarif.neben + "</b> je Nebenkosten-Aufruf. Die Regler ändern erst dann etwas, wenn du sie unten als aktive Preise speicherst.";
}
}
function setzeSlider(satz, wert) {
var slider = document.getElementById("vr-slider-" + satz);
if (slider) {
if (wert > parseInt(slider.max, 10)) slider.max = wert;
slider.value = wert;
}
}
function ladeTarif() {
if (typeof API === "undefined") return;
API.get("/api/pricing/tariff").then(function (data) {
var t = (data && data.tariff) || {};
aktivTarif = {
live: Math.round(t.monitor_adhoc || saetze.live),
research: Math.round(t.monitor_research || saetze.research),
neben: Math.round(t.chat || saetze.neben),
};
// Regler starten auf den aktiven Preisen
saetze.live = aktivTarif.live;
saetze.research = aktivTarif.research;
saetze.neben = aktivTarif.neben;
setzeSlider("live", saetze.live);
setzeSlider("research", saetze.research);
setzeSlider("neben", saetze.neben);
satzGesetzt();
renderAktivHint();
}).catch(function () { /* Hinweis bleibt generisch */ });
}
function speichereTarif() {
var msg = document.getElementById("vr-tarif-msg");
var tuEs = function () {
if (msg) msg.textContent = "Speichern …";
API.put("/api/pricing/tariff", {
monitor_adhoc: saetze.live,
monitor_research: saetze.research,
chat: saetze.neben,
enhance: saetze.neben,
globe: saetze.neben,
}).then(function () {
aktivTarif = { live: saetze.live, research: saetze.research, neben: saetze.neben };
renderAktivHint();
if (typeof loadTariffInfo === "function") loadTariffInfo();
if (msg) msg.textContent = "Gespeichert, gilt ab sofort für alle neuen Buchungen.";
setTimeout(function () { if (msg) msg.textContent = ""; }, 6000);
}).catch(function (err) {
if (msg) msg.textContent = "Fehler: " + err.message;
});
};
if (typeof showConfirm === "function") {
showConfirm(
"Aktive Preise ändern?",
"Live-Lauf " + saetze.live + ", Recherche-Durchlauf " + saetze.research +
", Nebenkosten " + saetze.neben + " Credits gelten dann ab sofort für alle neuen Buchungen.",
tuEs
);
} else tuEs();
}
function renderEmpfehlung() {
if (!empfDaten) return;
["live", "research"].forEach(function (satz) {
@@ -344,13 +417,17 @@
return '<div class="vr-kopf">' +
"<h2>Verbrauchsrechner</h2>" +
"<p>Sätze frei einstellbar. Die Empfehlung stammt aus den echten API-Kosten der letzten 30 Tage (Median je Aktion geteilt durch 0,20 USD).</p>" +
'<p class="vr-basis"><strong>Reine Simulation.</strong> Die Regler ändern keine Sätze im Monitor, abgerechnet wird nach der zentralen Konfiguration (aktuell 45 je Live-Lauf, 40 je Recherche-Durchlauf, Nebenkosten 1).</p>' +
'<p class="vr-basis" id="vr-aktiv-hint"><strong>Simulation.</strong> Die Regler ändern erst dann etwas, wenn du sie unten als aktive Preise speicherst.</p>' +
"</div>" +
'<div class="vr-regler-grid">' +
reglerHtml("live", "Live-Monitoring", "Anlegen und Aktualisierung", 1, 100) +
reglerHtml("research", "Recherche", "Je Aktualisierung (Durchlauf)", 1, 100) +
reglerHtml("neben", "Nebenkosten", "Je Aufruf", 1, 10) +
"</div>" +
'<div style="margin:14px 0 4px; display:flex; align-items:center; gap:10px;">' +
'<button type="button" class="btn btn-primary" id="vr-tarif-speichern">Diese Sätze als aktive Preise speichern</button>' +
'<span id="vr-tarif-msg" style="font-size:13px; color:var(--text-secondary);"></span>' +
"</div>" +
'<p class="vr-basis">1 Credit = 0,20 USD Selbstkosten.</p>' +
'<div class="vr-calc">' +
"<div>" +
@@ -473,12 +550,16 @@
}
});
var speichernBtn = document.getElementById("vr-tarif-speichern");
if (speichernBtn) speichernBtn.addEventListener("click", speichereTarif);
updateWerte();
renderBlock("live");
renderBlock("recherche");
renderBlock("neben");
calc();
ladeEmpfehlung();
ladeTarif();
}
if (document.readyState === "loading") {