1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
248 lines
12 KiB
HTML
248 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Kosten-Hochrechnung pro Schüler:in/Jahr · GeoGraSim</title>
|
||
<style>
|
||
:root { --moss:#1f4b37; --sage:#4a7c4e; --sand:#e8c547; --beige:#e8e4d8; --ink:#2a2a2a; --muted:#6a6a6a; --line:rgba(0,0,0,.08); }
|
||
*{box-sizing:border-box}
|
||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--beige);color:var(--ink);margin:0;padding:1.5rem;line-height:1.5}
|
||
.wrap{max-width:980px;margin:0 auto}
|
||
a.back{color:var(--sage);text-decoration:none;font-size:.78rem}
|
||
a.back:hover{text-decoration:underline}
|
||
h1{font-size:1.6rem;margin:.4rem 0 .2rem;color:var(--moss);letter-spacing:-.01em}
|
||
.sub{color:var(--muted);font-size:.85rem;margin-bottom:1.5rem}
|
||
.panel{background:#fff;border-radius:10px;padding:1.1rem 1.2rem;border:1px solid var(--line);box-shadow:0 1px 2px rgba(0,0,0,.03);margin-bottom:1rem}
|
||
h2{font-size:.95rem;color:var(--moss);margin:0 0 .8rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em}
|
||
.params{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.7rem .9rem}
|
||
.param{display:flex;flex-direction:column;gap:.2rem}
|
||
.param label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-weight:600}
|
||
.param .row{display:flex;align-items:center;gap:.4rem}
|
||
.param input[type=number]{flex:1;padding:.45rem .55rem;border:1px solid var(--line);border-radius:6px;font-size:.9rem;font-family:inherit;font-variant-numeric:tabular-nums}
|
||
.param input[type=range]{width:100%}
|
||
.param .unit{font-size:.75rem;color:var(--muted);min-width:3em}
|
||
.param .hint{font-size:.66rem;color:#a0a0a0}
|
||
table{width:100%;border-collapse:collapse;font-size:.85rem}
|
||
th,td{padding:.55rem .6rem;text-align:left;border-bottom:1px solid var(--line)}
|
||
th{font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:700;background:#fafaf6}
|
||
td.num{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
|
||
td.eur{text-align:right;font-variant-numeric:tabular-nums;font-weight:800;color:var(--moss)}
|
||
tr.highlight{background:#f4f9f0}
|
||
.legend{font-size:.7rem;color:var(--muted);margin-top:.6rem}
|
||
.big{font-size:1.8rem;font-weight:800;color:var(--moss);font-variant-numeric:tabular-nums}
|
||
.big-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
|
||
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.8rem;margin-top:.4rem}
|
||
.stat{text-align:center;padding:.6rem;background:#f7faf3;border-radius:8px;border:1px solid rgba(74,124,78,.15)}
|
||
.note{font-size:.72rem;color:var(--muted);margin-top:.7rem;line-height:1.5}
|
||
.row-meta{font-size:.7rem;color:var(--muted);margin-top:.2rem}
|
||
details{margin-top:.5rem}
|
||
summary{font-size:.78rem;color:var(--sage);cursor:pointer;font-weight:600}
|
||
details ul{font-size:.78rem;color:var(--ink);padding-left:1.2rem;margin:.4rem 0}
|
||
details li{margin-bottom:.2rem}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<a class="back" href="../">← Local Developer Tools</a>
|
||
<h1>💰 Kosten pro Schüler:in / Jahr</h1>
|
||
<p class="sub">Hochrechnung der DigitalOcean-Hosting-Kosten basierend auf gemessenen DB-Schreibmengen von Heli, Klima & restlichen Sims. Werte sind live einstellbar.</p>
|
||
|
||
<div class="panel">
|
||
<h2>Rahmenbedingungen</h2>
|
||
<div class="params">
|
||
<div class="param">
|
||
<label for="users">Userbasis</label>
|
||
<div class="row"><input type="number" id="users" min="1000" max="500000" step="1000" value="100000"><span class="unit">User</span></div>
|
||
<input type="range" min="1000" max="500000" step="1000" value="100000" oninput="document.getElementById('users').value=this.value;recalc()">
|
||
</div>
|
||
<div class="param">
|
||
<label for="hoursPerWeek">Spielzeit pro Woche</label>
|
||
<div class="row"><input type="number" id="hoursPerWeek" min="0.25" max="10" step="0.25" value="1"><span class="unit">h/Woche</span></div>
|
||
<div class="hint">Schulische Nutzung im Mittel</div>
|
||
</div>
|
||
<div class="param">
|
||
<label for="weeksPerYear">Schulwochen pro Jahr</label>
|
||
<div class="row"><input type="number" id="weeksPerYear" min="1" max="52" step="1" value="36"><span class="unit">Wochen</span></div>
|
||
<div class="hint">36 = AT-Schuljahr, 40 = aktiver Einsatz</div>
|
||
</div>
|
||
<div class="param">
|
||
<label for="kbPerHour">DB-Schreiblast/Stunde</label>
|
||
<div class="row"><input type="number" id="kbPerHour" min="1" max="500" step="1" value="50"><span class="unit">KB/h</span></div>
|
||
<div class="hint">Status quo 50 / mit Aggregation ~10</div>
|
||
</div>
|
||
<div class="param">
|
||
<label for="storagePrice">Storage-Preis</label>
|
||
<div class="row"><input type="number" id="storagePrice" min="0.05" max="1.0" step="0.05" value="0.20"><span class="unit">$/GB/Monat</span></div>
|
||
<div class="hint">DigitalOcean Managed MySQL Tier</div>
|
||
</div>
|
||
<div class="param">
|
||
<label for="basecost">Fix-Infrastruktur</label>
|
||
<div class="row"><input type="number" id="basecost" min="0" max="2000" step="10" value="160"><span class="unit">$/Monat</span></div>
|
||
<div class="hint">App-Server + DB-Cluster-Base + Backups</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>Resultat</h2>
|
||
<div class="summary">
|
||
<div class="stat">
|
||
<div class="big-label">DB-Speicher pro Jahr</div>
|
||
<div class="big" id="r-storage">– GB</div>
|
||
<div class="row-meta" id="r-storage-detail">–</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="big-label">Gesamtkosten pro Jahr</div>
|
||
<div class="big" id="r-total">– $</div>
|
||
<div class="row-meta" id="r-total-detail">Storage + Fix-Kosten</div>
|
||
</div>
|
||
<div class="stat" style="background:#fff8e0;border-color:rgba(232,197,71,.4)">
|
||
<div class="big-label">pro Schüler:in / Jahr</div>
|
||
<div class="big" id="r-per-user">– ¢</div>
|
||
<div class="row-meta" id="r-per-user-detail">Vollkosten geteilt durch Userbasis</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>Vergleichsszenarien (bei aktueller Eingabe)</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Skala</th>
|
||
<th class="num">Schüler:innen</th>
|
||
<th class="num">DB-Volumen/Jahr</th>
|
||
<th class="num">Vollkosten/Jahr</th>
|
||
<th class="num">pro Person</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="scenarios"></tbody>
|
||
</table>
|
||
<div class="legend">Annahme: Fix-Infrastruktur skaliert grob mit log<sub>10</sub>(Userbasis). Storage linear.</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>Status quo vs. Aggregation</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Variante</th>
|
||
<th class="num">DB-Volumen/Jahr</th>
|
||
<th class="num">Storage-Kosten</th>
|
||
<th class="num">Vollkosten</th>
|
||
<th class="num">pro Person/Jahr</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="aggr"></tbody>
|
||
</table>
|
||
<details>
|
||
<summary>Was bedeutet „Aggregation"?</summary>
|
||
<ul>
|
||
<li><strong>Live (im Spiel):</strong> 1 Row pro aktiver Schüler:in × Sim, UPSERT → kein Wachstum.</li>
|
||
<li><strong>Tagespuffer (24 h):</strong> Heartbeat-Rows landen in `live_pings`, werden nach 24 h gelöscht.</li>
|
||
<li><strong>Tages-Archiv (30 Tage):</strong> 1 Row pro Schüler:in × Sim × Tag mit `total_minutes`, `best_stars`.</li>
|
||
<li><strong>Langzeit (Schuljahr):</strong> 1 Monats-Aggregat — sehr kompakt.</li>
|
||
</ul>
|
||
</details>
|
||
</div>
|
||
|
||
<div class="note">
|
||
<strong>Messgrundlage:</strong> 1 445 Heli-Assessment-Rows aus Prod-DB (Stand 06.06.2026) — durchschnittlich 340 Byte/Row, 97,8 % davon sind 30-s-Heartbeats. Klima schreibt selten, aber dafür 10 KB pro Final-Submit. Gewichteter Mittelwert: ~50 KB pro Spielstunde.
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function fmt(n, digits) {
|
||
digits = digits||0;
|
||
return n.toLocaleString('de-AT', {minimumFractionDigits:digits, maximumFractionDigits:digits});
|
||
}
|
||
function fmtBytes(gb) {
|
||
if (gb < 1) return fmt(gb*1024, 1) + ' MB';
|
||
if (gb < 1024) return fmt(gb, 1) + ' GB';
|
||
return fmt(gb/1024, 2) + ' TB';
|
||
}
|
||
function calcCosts(users, hoursPerWeek, weeks, kbPerHour, storagePrice, basecostMonthly) {
|
||
var hoursPerUserPerYear = hoursPerWeek * weeks;
|
||
var bytesPerUserPerYear = hoursPerUserPerYear * kbPerHour * 1024;
|
||
var totalBytesPerYear = bytesPerUserPerYear * users;
|
||
var totalGB = totalBytesPerYear / (1024*1024*1024);
|
||
var storageCostYear = totalGB * storagePrice * 12;
|
||
var fixCostYear = basecostMonthly * 12;
|
||
var totalYear = storageCostYear + fixCostYear;
|
||
var perUserCent = (totalYear / users) * 100; // $ → ¢
|
||
return { totalGB, storageCostYear, fixCostYear, totalYear, perUserCent, hoursPerUserPerYear, bytesPerUserPerYear };
|
||
}
|
||
function recalc() {
|
||
var users = +document.getElementById('users').value;
|
||
var hpw = +document.getElementById('hoursPerWeek').value;
|
||
var weeks = +document.getElementById('weeksPerYear').value;
|
||
var kbh = +document.getElementById('kbPerHour').value;
|
||
var sp = +document.getElementById('storagePrice').value;
|
||
var base = +document.getElementById('basecost').value;
|
||
|
||
var r = calcCosts(users, hpw, weeks, kbh, sp, base);
|
||
|
||
document.getElementById('r-storage').textContent = fmtBytes(r.totalGB);
|
||
document.getElementById('r-storage-detail').textContent =
|
||
fmt(r.bytesPerUserPerYear/1024, 0) + ' KB × ' + fmt(users, 0) + ' User';
|
||
document.getElementById('r-total').textContent = '$ ' + fmt(r.totalYear, 0);
|
||
document.getElementById('r-total-detail').textContent =
|
||
'Storage $' + fmt(r.storageCostYear, 0) + ' + Fix $' + fmt(r.fixCostYear, 0);
|
||
document.getElementById('r-per-user').textContent = fmt(r.perUserCent, 2) + ' ¢';
|
||
document.getElementById('r-per-user-detail').textContent =
|
||
'$ ' + fmt(r.perUserCent/100, 4) + ' / Jahr';
|
||
|
||
// Szenarien (verschiedene Userbasen, alles andere konstant)
|
||
var scen = [
|
||
{label:'Pilot-Schule', n:500, base: Math.max(15, base*0.10)},
|
||
{label:'1 Bezirk', n:5000, base: Math.max(40, base*0.30)},
|
||
{label:'1 Bundesland', n:25000, base: Math.max(100, base*0.65)},
|
||
{label:'AT 25 %', n:100000, base: base},
|
||
{label:'AT 100 %', n:400000, base: base*1.8},
|
||
];
|
||
var tbody = document.getElementById('scenarios');
|
||
tbody.innerHTML = '';
|
||
scen.forEach(function(s){
|
||
var rs = calcCosts(s.n, hpw, weeks, kbh, sp, s.base);
|
||
var tr = document.createElement('tr');
|
||
if (s.n === users) tr.className = 'highlight';
|
||
tr.innerHTML =
|
||
'<td><strong>'+s.label+'</strong></td>' +
|
||
'<td class="num">'+fmt(s.n,0)+'</td>' +
|
||
'<td class="num">'+fmtBytes(rs.totalGB)+'</td>' +
|
||
'<td class="num">$ '+fmt(rs.totalYear,0)+'</td>' +
|
||
'<td class="eur">'+fmt(rs.perUserCent,2)+' ¢</td>';
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
// Status quo vs Aggregation
|
||
var statusQuo = calcCosts(users, hpw, weeks, 50, sp, base);
|
||
var aggressive = calcCosts(users, hpw, weeks, 10, sp, base);
|
||
var rows = [
|
||
{label:'Status quo (Heli-Heartbeats 30 s in DB)', r: statusQuo},
|
||
{label:'Mit Aggregation (Heartbeats nach 24 h aggregiert)', r: aggressive},
|
||
];
|
||
var tbA = document.getElementById('aggr');
|
||
tbA.innerHTML = '';
|
||
rows.forEach(function(r,i){
|
||
var tr = document.createElement('tr');
|
||
if ((kbh > 25 && i===0) || (kbh <= 25 && i===1)) tr.className = 'highlight';
|
||
tr.innerHTML =
|
||
'<td><strong>'+r.label+'</strong></td>' +
|
||
'<td class="num">'+fmtBytes(r.r.totalGB)+'</td>' +
|
||
'<td class="num">$ '+fmt(r.r.storageCostYear,0)+'</td>' +
|
||
'<td class="num">$ '+fmt(r.r.totalYear,0)+'</td>' +
|
||
'<td class="eur">'+fmt(r.r.perUserCent,2)+' ¢</td>';
|
||
tbA.appendChild(tr);
|
||
});
|
||
}
|
||
// Inputs live an Re-Calc binden
|
||
['users','hoursPerWeek','weeksPerYear','kbPerHour','storagePrice','basecost']
|
||
.forEach(function(id){
|
||
document.getElementById(id).addEventListener('input', recalc);
|
||
});
|
||
recalc();
|
||
</script>
|
||
</body>
|
||
</html>
|