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 {
|
||||
"calls_per_month": calls_per_month,
|
||||
"cap": cap,
|
||||
"seconds_per_month": _SECONDS_PER_MONTH,
|
||||
"default_cadence_s": cls.default_cadence_s,
|
||||
"percent": percent,
|
||||
"warn": percent >= 80.0,
|
||||
"blocked": percent >= 100.0,
|
||||
|
|
|
|||
|
|
@ -66,10 +66,13 @@
|
|||
</div>
|
||||
|
||||
{% if quota %}
|
||||
<div class="quota-panel flash {% if quota.blocked %}flash-error{% elif quota.warn %}flash-warn{% endif %}">
|
||||
<strong>API quota:</strong> {{ quota.detail }}
|
||||
{% if quota.blocked %}<br>⛔ Over free-tier cap — reduce calls before saving.
|
||||
{% elif quota.warn %}<br>⚠️ Approaching free-tier cap.{% endif %}
|
||||
<div class="quota-panel flash {% if quota.blocked %}flash-error{% elif quota.warn %}flash-warn{% endif %}"
|
||||
data-field="{{ field.name }}"
|
||||
data-quota-cap="{{ quota.cap }}"
|
||||
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>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -103,3 +106,64 @@
|
|||
});
|
||||
})();
|
||||
</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 "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):
|
||||
from central.adapters.nws import NWSSettings
|
||||
s = {"contact_email": "a@b.com",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue