feat(pricing): Sätze kommen aus der Preistabelle, Rechner kann sie speichern

Die Credits-Sätze sind im Portal nicht mehr hart codiert. Neuer Endpoint
GET /api/pricing/tariff liest die geteilte Tabelle billing_tariff (mit
den beschlossenen Werten als Rückfallebene, falls die Tabelle fehlt),
PUT /api/pricing/tariff schreibt sie mit Validierung und Audit-Eintrag.

- Die Infozeile im Verbrauch-Bereich rendert ihre Zahlen aus der Tabelle,
  der statische Text bleibt nur als Rückfallebene stehen.
- Die Rechner-Regler starten auf den aktiven Preisen, der Hinweis nennt
  die tatsächlich abgerechneten Sätze, und der neue Knopf "Diese Sätze
  als aktive Preise speichern" schreibt sie nach Rückfrage in die
  Tabelle. Der Monitor bucht ab der nächsten Aktion mit den neuen Werten.
- Migration migrations/2026-07-25_billing_tariff.py für die eigene
  Staging-DB (auf Live legt die Tabelle der Monitor an und befüllt sie
  mit seinen wirksamen Konfigurationswerten).

Gegenstück im Monitor: Commit 027244a (billing_tariff als Quelle der
Buchung). Promote-Reihenfolge bleibt, erst Monitor, dann Verwaltung.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 14:32:25 +00:00
Ursprung a639579366
Commit c4ea949f91
5 geänderte Dateien mit 257 neuen und 5 gelöschten Zeilen

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") {