diff --git a/RELEASES.json b/RELEASES.json
index f505a29..338b62a 100644
--- a/RELEASES.json
+++ b/RELEASES.json
@@ -1,4 +1,15 @@
[
+ {
+ "version": "2026-07-24T20:37Z",
+ "date": "2026-07-24",
+ "title": "Neuer Verbrauchsrechner im Verwaltungsbereich",
+ "items": [
+ "Neuer Rechner-Tab zeigt Kosten übersichtlich nach Verbrauchsposten (Credits-Basierte Abrechnung)",
+ "Übersichtskarten mit aktuellen Credit-Sätzen direkt über dem Rechner",
+ "Credit-Sätze per Regler individuell anpassbar – mit Empfehlung aus den letzten 30 Tagen",
+ "Änderungen an den Reglern werden sofort in den Beschriftungen sichtbar"
+ ]
+ },
{
"version": "2026-05-22T12:41Z",
"date": "2026-05-22",
diff --git a/src/main.py b/src/main.py
index 84674c3..e780557 100644
--- a/src/main.py
+++ b/src/main.py
@@ -11,7 +11,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from config import STATIC_DIR, PORT
-from routers import auth, organizations, licenses, users, dashboard, sources, token_usage, audit, translation, x_scraper
+from routers import auth, organizations, licenses, users, dashboard, sources, token_usage, audit, translation, x_scraper, pricing
logging.basicConfig(
level=logging.INFO,
@@ -44,6 +44,7 @@ app.include_router(token_usage.router)
app.include_router(audit.router)
app.include_router(translation.router)
app.include_router(x_scraper.router)
+app.include_router(pricing.router)
# --- Statische Dateien ---
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
diff --git a/src/routers/pricing.py b/src/routers/pricing.py
new file mode 100644
index 0000000..5b10a96
--- /dev/null
+++ b/src/routers/pricing.py
@@ -0,0 +1,66 @@
+"""Preis-Empfehlung aus den echten API-Kosten der letzten Tage.
+
+Reine Leseabfrage. Die Empfehlung ist eine betreiberweite Kennzahl (Median der
+tatsaechlichen Refresh-Kosten je Lagentyp), deshalb wird die geteilte Live-DB
+read-only gelesen, unabhaengig vom DB_PATH des jeweiligen Portals.
+"""
+import logging
+import math
+import os
+import statistics
+from collections import defaultdict
+from datetime import datetime, timedelta
+
+import aiosqlite
+from fastapi import APIRouter, Depends
+from auth import get_current_admin
+
+logger = logging.getLogger("verwaltung.pricing")
+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
+
+
+@router.get("/cost-basis")
+async def cost_basis(days: int = 30, admin=Depends(get_current_admin)):
+ """Median der echten Refresh-Kosten je Lagentyp und daraus abgeleitete
+ Credit-Empfehlung (Median USD geteilt durch 0,20 USD, aufgerundet)."""
+ days = max(1, min(365, days))
+ cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
+ result = {"days": days, "usd_per_credit": USD_PER_CREDIT, "typen": {}}
+
+ try:
+ db = await aiosqlite.connect(f"file:{COST_BASIS_DB}?mode=ro", uri=True)
+ try:
+ cursor = await db.execute(
+ "SELECT i.type, r.total_cost_usd "
+ "FROM refresh_log r JOIN incidents i ON i.id = r.incident_id "
+ "WHERE r.status = 'completed' AND r.started_at >= ?",
+ (cutoff,),
+ )
+ rows = await cursor.fetchall()
+ finally:
+ await db.close()
+ except Exception as e:
+ logger.warning("cost-basis DB-Fehler: %s", e)
+ result["fehler"] = "Kostendaten nicht verfuegbar"
+ return result
+
+ gruppen = defaultdict(list)
+ for typ, kosten in rows:
+ gruppen[typ or "adhoc"].append(kosten or 0.0)
+
+ for db_key, label in (("adhoc", "live"), ("research", "research")):
+ werte = gruppen.get(db_key, [])
+ if werte:
+ median = statistics.median(werte)
+ result["typen"][label] = {
+ "median_usd": round(median, 2),
+ "n": len(werte),
+ "empfohlen": int(math.ceil(median / USD_PER_CREDIT)) if median > 0 else 0,
+ }
+ else:
+ result["typen"][label] = {"median_usd": None, "n": 0, "empfohlen": None}
+
+ return result
diff --git a/src/static/js/app.js b/src/static/js/app.js
index 2558b0b..cf47dab 100644
--- a/src/static/js/app.js
+++ b/src/static/js/app.js
@@ -1047,3 +1047,10 @@ document.addEventListener("DOMContentLoaded", () => {
loadMeta();
}
});
+
+// --- Verbrauchsrechner nachladen (eigenstaendiges Modul, siehe rechner.js) ---
+(function () {
+ var s = document.createElement("script");
+ s.src = "/static/js/rechner.js?v=20260724d";
+ document.head.appendChild(s);
+})();
diff --git a/src/static/js/rechner.js b/src/static/js/rechner.js
new file mode 100644
index 0000000..24fd0ab
--- /dev/null
+++ b/src/static/js/rechner.js
@@ -0,0 +1,488 @@
+"use strict";
+
+// Verbrauchsrechner (Posten-Modell) fuer das Verwaltungsportal.
+// Eigenstaendiges Modul, injiziert Tab, Bereich und Styles selbst,
+// damit dashboard.html unveraendert bleiben kann.
+// Saetze sind ueber Regler frei einstellbar, mit Empfehlung aus den echten
+// API-Kosten der letzten 30 Tage (GET /api/pricing/cost-basis).
+
+(function () {
+ var USD = 0.20;
+ var MINIMUM_TAKT_MINUTEN = 30;
+
+ // Frei einstellbare Saetze (Credits je Aktion).
+ var saetze = { live: 45, research: 40, neben: 1 };
+ function rechercheNeu() { return saetze.research * 3; }
+
+ var EINHEITEN = [
+ { wert: 1, text: "Minuten" },
+ { wert: 60, text: "Stunden" },
+ { wert: 1440, text: "Tage" }
+ ];
+
+ var BLOCK_NAMEN = { live: "Live-Monitoring", recherche: "Recherche", neben: "Nebenkosten" };
+ var BLOCK_REIHENFOLGE = ["live", "recherche", "neben"];
+
+ var bloecke = {
+ live: [{ anzahl: 1, nurManuell: false, taktWert: 1, taktEinheit: 1440, tage: 30, manuell: 0 }],
+ recherche: [{ anzahl: 1, vertiefungen: 1 }],
+ neben: [{ auto: true, aufrufe: 50 }]
+ };
+
+ function fmt(n) { return Math.round(n).toLocaleString("de-DE"); }
+ function fmtUsd(n) { return n.toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " $"; }
+
+ function neuerPosten(typ) {
+ if (typ === "recherche") return { anzahl: 1, vertiefungen: 1 };
+ if (typ === "neben") return { auto: true, aufrufe: 50 };
+ return { anzahl: 1, nurManuell: false, taktWert: 1, taktEinheit: 1440, tage: 30, manuell: 0 };
+ }
+
+ function autoAufrufe() {
+ var summe = 0;
+ bloecke.live.forEach(function (p) { summe += p.anzahl; });
+ bloecke.recherche.forEach(function (p) { summe += p.anzahl; });
+ return Math.round(summe * 1.5);
+ }
+
+ function postenCredits(typ, p) {
+ if (typ === "live") {
+ var laeufe = 1;
+ if (!p.nurManuell && p.taktWert > 0) {
+ var intervallMin = Math.max(MINIMUM_TAKT_MINUTEN, p.taktWert * p.taktEinheit);
+ laeufe = Math.max(1, Math.floor(p.tage * 1440 / intervallMin));
+ }
+ return (p.anzahl * laeufe + p.manuell) * saetze.live;
+ }
+ if (typ === "recherche") {
+ return p.anzahl * rechercheNeu() + p.vertiefungen * saetze.research;
+ }
+ return (p.auto ? autoAufrufe() : p.aufrufe) * saetze.neben;
+ }
+
+ // ---------- Posten-Felder ----------
+
+ function feldHtml(typ, i, name, labelText, hinweis, wert, min, max) {
+ var id = "vr-" + typ + "-" + i + "-" + name;
+ return '
' + labelText +
+ (hinweis ? " " + hinweis + " " : "") + " " +
+ '
';
+ }
+
+ function taktHtml(i, p) {
+ var idW = "vr-live-" + i + "-taktwert";
+ var idE = "vr-live-" + i + "-takteinheit";
+ var intervallMin = p.taktWert * p.taktEinheit;
+ var chips = [
+ { min: 60, text: "stündlich" },
+ { min: 720, text: "halbtäglich" },
+ { min: 1440, text: "täglich" }
+ ].map(function (c) {
+ return '" + c.text + " ";
+ }).join("");
+ return 'Takt wie oft jede Lage automatisch läuft, Minimum 30 Minuten ' +
+ '' +
+ ' ' +
+ '' +
+ EINHEITEN.map(function (o) {
+ return '" + o.text + " ";
+ }).join("") + "
" +
+ '' + chips + "
";
+ }
+
+ function postenFelder(typ, i, p) {
+ if (typ === "live") {
+ var idManuell = "vr-live-" + i + "-nurmanuell";
+ var html = feldHtml(typ, i, "anzahl", "Anzahl Lagen", "", p.anzahl, 1, 200) +
+ 'Nur manuell keine automatischen Läufe ' +
+ '
";
+ if (!p.nurManuell) {
+ html += taktHtml(i, p) +
+ feldHtml(typ, i, "tage", "Laufzeit in Tagen", "wie lange aktualisiert wird", p.tage, 1, 365);
+ }
+ html += feldHtml(typ, i, "manuell", "Aktualisierungen von Hand", "gesamt für diesen Posten", p.manuell, 0, 1000);
+ return html;
+ }
+ if (typ === "recherche") {
+ return feldHtml(typ, i, "anzahl", "Anzahl Recherchen", "", p.anzahl, 1, 200) +
+ feldHtml(typ, i, "vertiefungen", "Aktualisierungen von Hand", "gesamt für diesen Posten", p.vertiefungen, 0, 1000);
+ }
+ var idAuto = "vr-neben-" + i + "-auto";
+ var idAnz = "vr-neben-" + i + "-aufrufe";
+ return 'Automatisch berechnen Anzahl Lagen und Recherchen mal 1,5 ' +
+ '
" +
+ 'Anzahl Aufrufe Chat oder Beschreibungs-Assistent, je 1 Credit ' +
+ '
";
+ }
+
+ function renderBlock(typ) {
+ var el = document.getElementById("vr-liste-" + typ);
+ if (!el) return;
+ el.innerHTML = bloecke[typ].map(function (p, i) {
+ return '' +
+ '
' +
+ 'Posten ' + (i + 1) + " " +
+ '× ' +
+ "
" +
+ postenFelder(typ, i, p) +
+ '
= ' + fmt(postenCredits(typ, p)) + " Credits
" +
+ "
";
+ }).join("");
+ }
+
+ // ---------- Regler (frei einstellbare Saetze) ----------
+
+ function reglerHtml(satz, kicker, label, min, max) {
+ return '' +
+ '
' + kicker + "
" +
+ '
' + label + "
" +
+ '
' + saetze[satz] + " Credits
" +
+ '
' +
+ (satz === "live" || satz === "research" ? '
' : "") +
+ '
' +
+ (satz === "neben" ? "Nebenverbrauch real 0,02 bis 0,10 $ je Aufruf. 1 Credit ist die kleinste sinnvolle Einheit." : "Empfehlung wird geladen…") +
+ "
" +
+ "
";
+ }
+
+ function updateWerte() {
+ var wL = document.getElementById("vr-wert-live"); if (wL) wL.textContent = saetze.live;
+ var wR = document.getElementById("vr-wert-research"); if (wR) wR.textContent = saetze.research;
+ var wN = document.getElementById("vr-wert-neben"); if (wN) wN.textContent = saetze.neben;
+ var noteL = document.getElementById("vr-note-live");
+ if (noteL) noteL.textContent = "Gilt für Anlegen und jede Aktualisierung, gleicher Satz.";
+ var noteR = document.getElementById("vr-note-research");
+ if (noteR) noteR.textContent = "Anlegen einer Recherche = 3 × " + saetze.research + " = " + rechercheNeu() + " Credits.";
+ }
+
+ var empfDaten = null;
+
+ function renderEmpfehlung() {
+ if (!empfDaten) return;
+ ["live", "research"].forEach(function (satz) {
+ var el = document.getElementById("vr-empf-" + satz);
+ if (!el) return;
+ var t = (empfDaten.typen || {})[satz];
+ if (!t || t.empfohlen == null || !t.median_usd) {
+ el.textContent = "Keine Kostendaten der letzten " + empfDaten.days + " Tage.";
+ return;
+ }
+ el.innerHTML = "Empfohlen " + t.empfohlen + " · Median " + fmtUsd(t.median_usd) +
+ " aus " + fmt(t.n) + " Läufen (" + empfDaten.days + " Tage) " +
+ 'übernehmen ';
+ });
+ }
+
+ function ladeEmpfehlung() {
+ var token = null;
+ try { token = localStorage.getItem("token"); } catch (e) {}
+ fetch("/api/pricing/cost-basis?days=30", {
+ headers: token ? { "Authorization": "Bearer " + token } : {}
+ }).then(function (r) {
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ return r.json();
+ }).then(function (data) {
+ empfDaten = data;
+ renderEmpfehlung();
+ }).catch(function () {
+ ["live", "research"].forEach(function (satz) {
+ var el = document.getElementById("vr-empf-" + satz);
+ if (el) el.textContent = "Empfehlung derzeit nicht verfügbar.";
+ });
+ });
+ }
+
+ // ---------- Gesamtrechnung ----------
+
+ function calc() {
+ var total = 0;
+ var zeilen = [];
+ BLOCK_REIHENFOLGE.forEach(function (typ) {
+ bloecke[typ].forEach(function (p, i) {
+ var c = postenCredits(typ, p);
+ total += c;
+ zeilen.push('' + BLOCK_NAMEN[typ] + " " + (i + 1) + " " + fmt(c) + "
");
+ });
+ });
+ var posten = document.getElementById("vr-posten-zeilen");
+ if (posten) posten.innerHTML = zeilen.length ? zeilen.join("") : 'Noch keine Posten 0
';
+ var totalEl = document.getElementById("vr-total");
+ if (totalEl) totalEl.innerHTML = fmt(total) + "Credits insgesamt ";
+ var usdEl = document.getElementById("vr-usd");
+ if (usdEl) usdEl.textContent = fmtUsd(total * USD);
+ syncNebenAnzeige();
+ }
+
+ function syncNebenAnzeige() {
+ bloecke.neben.forEach(function (p, i) {
+ if (!p.auto) return;
+ var wrap = document.querySelector('#vr-liste-neben .vr-posten[data-i="' + i + '"]');
+ if (!wrap) return;
+ var input = wrap.querySelector('input[data-feld="aufrufe"]');
+ if (input) input.value = autoAufrufe();
+ var sumEl = wrap.querySelector("[data-sum]");
+ if (sumEl) sumEl.textContent = fmt(postenCredits("neben", p));
+ });
+ }
+
+ function alleSummenNeu() {
+ BLOCK_REIHENFOLGE.forEach(function (typ) {
+ bloecke[typ].forEach(function (p, i) {
+ var wrap = document.querySelector("#vr-liste-" + typ + ' .vr-posten[data-i="' + i + '"]');
+ if (!wrap) return;
+ var sumEl = wrap.querySelector("[data-sum]");
+ if (sumEl) sumEl.textContent = fmt(postenCredits(typ, p));
+ });
+ });
+ }
+
+ function satzGesetzt() {
+ updateWerte();
+ alleSummenNeu();
+ calc();
+ }
+
+ function wertUebernehmen(e) {
+ var wrap = e.target.closest(".vr-posten");
+ var name = e.target.getAttribute("data-feld");
+ if (!wrap || !name) return;
+ var typ = wrap.getAttribute("data-typ");
+ var i = parseInt(wrap.getAttribute("data-i"), 10);
+ if (name === "auto") {
+ if (!e.target.checked) bloecke.neben[i].aufrufe = autoAufrufe();
+ bloecke.neben[i].auto = e.target.checked;
+ renderBlock("neben");
+ calc();
+ return;
+ }
+ if (name === "nurManuell") {
+ bloecke.live[i].nurManuell = e.target.checked;
+ renderBlock("live");
+ calc();
+ return;
+ }
+ bloecke[typ][i][name] = parseFloat(e.target.value) || 0;
+ var sumEl = wrap.querySelector("[data-sum]");
+ if (sumEl) sumEl.textContent = fmt(postenCredits(typ, bloecke[typ][i]));
+ calc();
+ }
+
+ function normalisiereTakt(el) {
+ var wrap = el.closest(".vr-posten");
+ if (!wrap) return;
+ var i = parseInt(wrap.getAttribute("data-i"), 10);
+ var p = bloecke.live[i];
+ if (!p || p.nurManuell || p.taktWert <= 0) return;
+ var intervallMin = p.taktWert * p.taktEinheit;
+ if (intervallMin >= MINIMUM_TAKT_MINUTEN) return;
+ p.taktWert = Math.ceil((MINIMUM_TAKT_MINUTEN / p.taktEinheit) * 100) / 100;
+ var input = wrap.querySelector('input[data-feld="taktWert"]');
+ if (input) input.value = p.taktWert;
+ var sumEl = wrap.querySelector("[data-sum]");
+ if (sumEl) sumEl.textContent = fmt(postenCredits("live", p));
+ calc();
+ }
+
+ // ---------- Styles ----------
+
+ var STYLES =
+ ".vr-kopf { margin-bottom: 16px; }" +
+ ".vr-kopf h2 { margin: 0 0 4px; font-size: 20px; }" +
+ ".vr-kopf p { margin: 0; font-size: 13px; color: var(--text-secondary); }" +
+ ".vr-regler-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin-bottom: 10px; }" +
+ ".vr-regler { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }" +
+ ".vr-regler-kicker { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); margin-bottom: 6px; }" +
+ ".vr-regler-label { font-size: 14px; font-weight: 600; margin-bottom: 6px; }" +
+ ".vr-regler-wert { font-size: 30px; font-weight: 700; color: var(--accent); line-height: 1.1; margin-bottom: 8px; }" +
+ ".vr-regler-wert small { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-left: 4px; }" +
+ ".vr-regler input[type=range] { width: 100%; accent-color: var(--accent); cursor: pointer; }" +
+ ".vr-note { font-size: 12px; color: var(--text-secondary); margin-top: 6px; }" +
+ ".vr-empf { font-size: 12px; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }" +
+ ".vr-empf b { color: var(--accent); }" +
+ ".vr-uebernehmen { margin-left: 4px; padding: 2px 9px; font-family: inherit; font-size: 11px; background: var(--bg-tertiary); color: var(--accent); border: 1px solid var(--accent); border-radius: 999px; cursor: pointer; }" +
+ ".vr-uebernehmen:hover { background: var(--accent); color: var(--bg-primary); }" +
+ ".vr-basis { font-size: 13px; color: var(--text-secondary); margin: 0 0 20px; }" +
+ ".vr-calc { display: grid; grid-template-columns: 1fr 320px; gap: 20px; align-items: start; }" +
+ "@media (max-width: 900px) { .vr-calc { grid-template-columns: 1fr; } }" +
+ ".vr-block { margin-bottom: 24px; }" +
+ ".vr-block-title { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); margin: 0 0 10px; }" +
+ ".vr-posten { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 10px; }" +
+ ".vr-head { display: flex; align-items: center; margin-bottom: 10px; }" +
+ ".vr-title { font-weight: 600; font-size: 13px; }" +
+ ".vr-remove { margin-left: auto; width: 26px; height: 26px; background: transparent; border: 1px solid var(--border); border-radius: 6px; color: var(--text-secondary); font-size: 15px; line-height: 1; cursor: pointer; }" +
+ ".vr-remove:hover { border-color: var(--danger); color: var(--danger); }" +
+ ".vr-field { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }" +
+ ".vr-field label { font-size: 13px; color: var(--text-primary); }" +
+ ".vr-field label small { display: block; font-size: 11px; color: var(--text-muted); }" +
+ ".vr-field input[type=number], .vr-field select { width: 110px; flex: none; padding: 6px 8px; background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px; font-family: inherit; font-size: 13px; }" +
+ ".vr-field input[type=number]::-webkit-outer-spin-button, .vr-field input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }" +
+ ".vr-field input[type=number] { -moz-appearance: textfield; appearance: textfield; }" +
+ ".vr-field input[type=checkbox] { width: 16px; height: 16px; flex: none; accent-color: var(--accent); cursor: pointer; }" +
+ ".vr-field input:disabled { opacity: 0.55; }" +
+ ".vr-field input:focus, .vr-field select:focus { outline: none; border-color: var(--accent); }" +
+ ".vr-takt { display: flex; gap: 6px; flex: none; }" +
+ ".vr-takt input { width: 58px; }" +
+ ".vr-takt select { width: 96px; }" +
+ ".vr-schnell { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; margin: -2px 0 10px; }" +
+ ".vr-schnell button { padding: 2px 10px; font-family: inherit; font-size: 11px; background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 999px; cursor: pointer; }" +
+ ".vr-schnell button:hover, .vr-schnell button.aktiv { border-color: var(--accent); color: var(--accent); }" +
+ ".vr-sum { border-top: 1px solid var(--bg-tertiary); margin-top: 10px; padding-top: 8px; text-align: right; font-size: 13px; color: var(--text-secondary); }" +
+ ".vr-sum b { color: var(--accent); font-size: 14px; }" +
+ ".vr-add { width: 100%; padding: 9px; background: transparent; border: 1px dashed var(--accent); border-radius: 8px; color: var(--accent); font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }" +
+ ".vr-add:hover { background: rgba(240, 180, 41, 0.08); }" +
+ ".vr-result { background: var(--bg-secondary); border: 1px solid var(--accent); border-radius: 8px; padding: 16px; position: sticky; top: 16px; }" +
+ ".vr-total { font-size: 30px; font-weight: 700; color: var(--accent); line-height: 1.1; }" +
+ ".vr-total small { display: block; font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-top: 4px; }" +
+ ".vr-row { display: flex; justify-content: space-between; font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--bg-tertiary); }" +
+ ".vr-row:last-child { border-bottom: none; }" +
+ ".vr-row span:last-child { font-weight: 600; font-variant-numeric: tabular-nums; }" +
+ ".vr-sep { margin: 12px 0; border: none; border-top: 1px solid var(--border); }";
+
+ // ---------- Aufbau ----------
+
+ function sektionHtml() {
+ return '' +
+ "
Verbrauchsrechner " +
+ "
Sätze frei einstellbar. Die Empfehlung stammt aus den echten API-Kosten der letzten 30 Tage (Median je Aktion geteilt durch 0,20 USD).
" +
+ "
" +
+ '' +
+ reglerHtml("live", "Live-Monitoring", "Anlegen und Aktualisierung", 1, 100) +
+ reglerHtml("research", "Recherche", "Je Aktualisierung (Durchlauf)", 1, 100) +
+ reglerHtml("neben", "Nebenkosten", "Je Aufruf", 1, 10) +
+ "
" +
+ '1 Credit = 0,20 USD Selbstkosten.
' +
+ '' +
+ "
" +
+ '
Live-Monitoring
' +
+ '
+ Live-Posten hinzufügen ' +
+ '
Recherche
' +
+ '
+ Recherche-Posten hinzufügen ' +
+ '
Nebenkosten
' +
+ '
+ Nebenkosten-Posten hinzufügen ' +
+ "
" +
+ '
' +
+ '
0Credits insgesamt
' +
+ '
' +
+ '
' +
+ '
' +
+ '
Selbstkosten 0 $
' +
+ "
" +
+ "
";
+ }
+
+ function init() {
+ var nav = document.querySelector("nav.nav-tabs");
+ var content = document.querySelector(".app-content");
+ if (!nav || !content || document.getElementById("sec-rechner")) return;
+
+ var styleEl = document.createElement("style");
+ styleEl.textContent = STYLES;
+ document.head.appendChild(styleEl);
+
+ var sektion = document.createElement("div");
+ sektion.className = "section";
+ sektion.id = "sec-rechner";
+ sektion.innerHTML = sektionHtml();
+ content.appendChild(sektion);
+
+ var btn = document.createElement("button");
+ btn.className = "nav-tab";
+ btn.setAttribute("data-section", "rechner");
+ btn.textContent = "Verbrauchsrechner";
+ nav.appendChild(btn);
+
+ btn.addEventListener("click", function () {
+ document.querySelectorAll("nav.nav-tabs .nav-tab").forEach(function (t) { t.classList.remove("active"); });
+ btn.classList.add("active");
+ document.querySelectorAll(".app-content > .section").forEach(function (s) { s.classList.remove("active"); });
+ sektion.classList.add("active");
+ var detail = document.getElementById("orgDetail");
+ if (detail) detail.classList.remove("active");
+ });
+
+ nav.addEventListener("click", function (e) {
+ var ziel = e.target.closest(".nav-tab");
+ if (!ziel || ziel === btn) return;
+ btn.classList.remove("active");
+ sektion.classList.remove("active");
+ });
+
+ // Regler-Slider live in die Saetze uebernehmen.
+ sektion.addEventListener("input", function (e) {
+ if (e.target.type === "range") {
+ var satz = e.target.getAttribute("data-satz");
+ saetze[satz] = parseInt(e.target.value, 10) || 1;
+ satzGesetzt();
+ return;
+ }
+ if (e.target.tagName === "SELECT") return;
+ wertUebernehmen(e);
+ });
+
+ sektion.addEventListener("change", function (e) {
+ var name = e.target.getAttribute("data-feld");
+ if (e.target.tagName === "SELECT") wertUebernehmen(e);
+ if (name === "taktWert" || name === "taktEinheit") normalisiereTakt(e.target);
+ });
+
+ sektion.addEventListener("click", function (e) {
+ var uebBtn = e.target.closest("[data-uebernehmen]");
+ if (uebBtn) {
+ var s = uebBtn.getAttribute("data-uebernehmen");
+ var w = parseInt(uebBtn.getAttribute("data-wert"), 10);
+ if (w > 0) {
+ saetze[s] = w;
+ var slider = document.getElementById("vr-slider-" + s);
+ if (slider) {
+ if (w > parseInt(slider.max, 10)) slider.max = w;
+ slider.value = w;
+ }
+ satzGesetzt();
+ }
+ return;
+ }
+ var taktBtn = e.target.closest("[data-takt]");
+ if (taktBtn) {
+ var wrapT = taktBtn.closest(".vr-posten");
+ var iT = parseInt(wrapT.getAttribute("data-i"), 10);
+ var minuten = parseInt(taktBtn.getAttribute("data-takt"), 10);
+ var pT = bloecke.live[iT];
+ if (minuten % 1440 === 0) { pT.taktWert = minuten / 1440; pT.taktEinheit = 1440; }
+ else if (minuten % 60 === 0) { pT.taktWert = minuten / 60; pT.taktEinheit = 60; }
+ else { pT.taktWert = minuten; pT.taktEinheit = 1; }
+ renderBlock("live");
+ calc();
+ return;
+ }
+ var addBtn = e.target.closest("[data-add]");
+ if (addBtn) {
+ var typNeu = addBtn.getAttribute("data-add");
+ bloecke[typNeu].push(neuerPosten(typNeu));
+ renderBlock(typNeu);
+ calc();
+ return;
+ }
+ var removeBtn = e.target.closest("[data-remove]");
+ if (removeBtn) {
+ var wrap = removeBtn.closest(".vr-posten");
+ var typ = wrap.getAttribute("data-typ");
+ bloecke[typ].splice(parseInt(wrap.getAttribute("data-i"), 10), 1);
+ renderBlock(typ);
+ calc();
+ }
+ });
+
+ updateWerte();
+ renderBlock("live");
+ renderBlock("recherche");
+ renderBlock("neben");
+ calc();
+ ladeEmpfehlung();
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+ } else {
+ init();
+ }
+})();