mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
v0.9.15: live JS quota recompute on multi-bbox editor
Mirror TomTomIncidentsAdapter.quota_estimate in client-side JS so the quota panel updates live as the operator edits a per-row cadence or the adapter cadence, and on Add/Delete bbox -- instead of only on Save. - tomtom_incidents.py: expose seconds_per_month + default_cadence_s in the quota dict (no JS magic numbers). - model_list.html: data-quota-* attrs + .quota-detail/.quota-msg spans; self-contained DOMContentLoaded-guarded IIFE applying the same max(adapter_cadence, bbox_cadence||default) floor and 80%/100% warn/block thresholds. No-op when no quota panel present. - tests: structural asserts for data-attrs + span hooks (JS exec needs a browser; live behaviour eyeballed manually). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
91f8478f80
commit
f52e545ddf
3 changed files with 91 additions and 4 deletions
|
|
@ -333,6 +333,8 @@ class TomTomIncidentsAdapter(SourceAdapter):
|
||||||
return {
|
return {
|
||||||
"calls_per_month": calls_per_month,
|
"calls_per_month": calls_per_month,
|
||||||
"cap": cap,
|
"cap": cap,
|
||||||
|
"seconds_per_month": _SECONDS_PER_MONTH,
|
||||||
|
"default_cadence_s": cls.default_cadence_s,
|
||||||
"percent": percent,
|
"percent": percent,
|
||||||
"warn": percent >= 80.0,
|
"warn": percent >= 80.0,
|
||||||
"blocked": percent >= 100.0,
|
"blocked": percent >= 100.0,
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if quota %}
|
{% if quota %}
|
||||||
<div class="quota-panel flash {% if quota.blocked %}flash-error{% elif quota.warn %}flash-warn{% endif %}">
|
<div class="quota-panel flash {% if quota.blocked %}flash-error{% elif quota.warn %}flash-warn{% endif %}"
|
||||||
<strong>API quota:</strong> {{ quota.detail }}
|
data-field="{{ field.name }}"
|
||||||
{% if quota.blocked %}<br>⛔ Over free-tier cap — reduce calls before saving.
|
data-quota-cap="{{ quota.cap }}"
|
||||||
{% elif quota.warn %}<br>⚠️ Approaching free-tier cap.{% endif %}
|
data-quota-spm="{{ quota.seconds_per_month }}"
|
||||||
|
data-quota-default="{{ quota.default_cadence_s }}">
|
||||||
|
<strong>API quota:</strong> <span class="quota-detail">{{ quota.detail }}</span>
|
||||||
|
<span class="quota-msg">{% if quota.blocked %}<br>⛔ Over free-tier cap — reduce calls before saving.{% elif quota.warn %}<br>⚠️ Approaching free-tier cap.{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
@ -103,3 +106,64 @@
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// v0.9.15: live client-side mirror of TomTomIncidentsAdapter.quota_estimate --
|
||||||
|
// recompute the est. monthly call count as the operator edits a cadence input
|
||||||
|
// or adds/removes a bbox row, without waiting for Save. Vanilla, self-contained,
|
||||||
|
// and a no-op when no quota panel is present (non-TomTom model_lists).
|
||||||
|
(function() {
|
||||||
|
function init() {
|
||||||
|
var panel = document.querySelector('.quota-panel[data-field]');
|
||||||
|
if (!panel) return;
|
||||||
|
var container = document.querySelector(
|
||||||
|
'.model-list[data-field="' + panel.dataset.field + '"]');
|
||||||
|
if (!container) return;
|
||||||
|
var body = container.querySelector('.model-list-body');
|
||||||
|
if (!body) return;
|
||||||
|
var CAP = parseFloat(panel.dataset.quotaCap);
|
||||||
|
var SPM = parseFloat(panel.dataset.quotaSpm);
|
||||||
|
var DEF = parseFloat(panel.dataset.quotaDefault);
|
||||||
|
var detailEl = panel.querySelector('.quota-detail');
|
||||||
|
var msgEl = panel.querySelector('.quota-msg');
|
||||||
|
|
||||||
|
// Server uses the adapter-level cadence as a floor:
|
||||||
|
// effective = max(adapter_cadence, bbox_cadence || default).
|
||||||
|
function adapterCadence() {
|
||||||
|
var el = document.getElementById('cadence_s');
|
||||||
|
var v = el ? parseFloat(el.value) : NaN;
|
||||||
|
return isNaN(v) ? 0 : v;
|
||||||
|
}
|
||||||
|
function recompute() {
|
||||||
|
var rows = body.querySelectorAll('.model-list-row');
|
||||||
|
var floor = adapterCadence(), calls = 0;
|
||||||
|
rows.forEach(function(row) {
|
||||||
|
var el = row.querySelector('[data-sub="cadence_s"]');
|
||||||
|
var bc = el ? parseFloat(el.value) : NaN;
|
||||||
|
var eff = Math.max(floor, isNaN(bc) ? DEF : bc);
|
||||||
|
if (eff > 0) calls += SPM / eff;
|
||||||
|
});
|
||||||
|
var cpm = Math.round(calls);
|
||||||
|
var pct = CAP ? (cpm / CAP * 100) : 0;
|
||||||
|
var warn = pct >= 80, blocked = pct >= 100;
|
||||||
|
detailEl.textContent = cpm.toLocaleString() + ' est. calls/month across '
|
||||||
|
+ rows.length + ' bbox(es) vs ' + CAP.toLocaleString()
|
||||||
|
+ '/month free tier (' + Math.round(pct) + '%)';
|
||||||
|
msgEl.innerHTML = blocked
|
||||||
|
? '<br>⛔ Over free-tier cap — reduce calls before saving.'
|
||||||
|
: (warn ? '<br>⚠️ Approaching free-tier cap.' : '');
|
||||||
|
panel.classList.toggle('flash-error', blocked);
|
||||||
|
panel.classList.toggle('flash-warn', warn && !blocked);
|
||||||
|
}
|
||||||
|
body.addEventListener('input', recompute);
|
||||||
|
var core = document.getElementById('cadence_s');
|
||||||
|
if (core) core.addEventListener('input', recompute);
|
||||||
|
new MutationObserver(recompute).observe(body, { childList: true });
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,27 @@ class TestModelListRender:
|
||||||
assert "model-list" in out and 'name="bboxes-0-name"' in out
|
assert "model-list" in out and 'name="bboxes-0-name"' in out
|
||||||
assert "free tier" in out # quota panel rendered
|
assert "free tier" in out # quota panel rendered
|
||||||
|
|
||||||
|
def test_quota_panel_exposes_client_recompute_constants(self):
|
||||||
|
# v0.9.15: data-quota-* attrs let the client JS mirror the server
|
||||||
|
# formula with no hardcoded magic numbers.
|
||||||
|
fields = describe_fields(TomTomIncidentsAdapter.settings_schema, _SETTINGS)
|
||||||
|
quota = TomTomIncidentsAdapter.quota_estimate(TomTomIncidentsSettings(**_SETTINGS), 1800)
|
||||||
|
out = _render("adapters_edit.html", _ctx(_SETTINGS, fields, quota))
|
||||||
|
assert 'data-quota-cap="2500"' in out
|
||||||
|
assert 'data-quota-spm="2592000"' in out
|
||||||
|
assert 'data-quota-default="1800"' in out
|
||||||
|
|
||||||
|
def test_quota_panel_wraps_detail_and_msg_for_live_update(self):
|
||||||
|
# v0.9.15: detail/msg split into spans so the IIFE can rewrite the text
|
||||||
|
# and warn/block state live. JS exec needs a browser, so this is
|
||||||
|
# structural only -- live behaviour (cadence edit + Add/Delete row)
|
||||||
|
# was eyeballed manually against /adapters/tomtom_incidents.
|
||||||
|
fields = describe_fields(TomTomIncidentsAdapter.settings_schema, _SETTINGS)
|
||||||
|
quota = TomTomIncidentsAdapter.quota_estimate(TomTomIncidentsSettings(**_SETTINGS), 1800)
|
||||||
|
out = _render("adapters_edit.html", _ctx(_SETTINGS, fields, quota))
|
||||||
|
assert '<span class="quota-detail">' in out
|
||||||
|
assert '<span class="quota-msg">' in out
|
||||||
|
|
||||||
def test_nws_region_intact_no_model_list(self):
|
def test_nws_region_intact_no_model_list(self):
|
||||||
from central.adapters.nws import NWSSettings
|
from central.adapters.nws import NWSSettings
|
||||||
s = {"contact_email": "a@b.com",
|
s = {"contact_email": "a@b.com",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue