Aug 21 - Fix plan usage counter

This commit is contained in:
2026-08-21 09:40:25 -04:00
parent 6371b13c13
commit cb7c244872
4 changed files with 211 additions and 20 deletions
+22 -10
View File
@@ -219,20 +219,32 @@ def plan():
'allow_custom_domain': tenant.allow_custom_domain,
}
# Live counts (tenant DB)
# Live counts (tenant DB), one axis at a time.
#
# These used to be four calls inside a single dict literal in one
# try/except. Python evaluates every value before assigning, so ONE
# failing counter discarded the whole dict and the page rendered 0
# for all four axes — including users and facilities, which were
# fine. That is exactly how a wrong column name in the issues
# counter presented as "no usage number ever updates".
from app.tenancy.quota import (
count_active_users, count_active_facilities,
count_inspections_this_month, count_issues_this_month,
)
try:
quota_usage = {
'users': count_active_users(),
'facilities': count_active_facilities(),
'inspections': count_inspections_this_month(),
'issues': count_issues_this_month(),
}
except Exception as exc:
logger.error('tenant_settings.plan: quota count failed: %s', exc)
for axis, counter in (
('users', count_active_users),
('facilities', count_active_facilities),
('inspections', count_inspections_this_month),
('issues', count_issues_this_month),
):
try:
quota_usage[axis] = counter()
except Exception as exc:
# None (not 0) so the page shows "—": an unknown count and
# a genuine zero must not look the same.
quota_usage[axis] = None
logger.error('tenant_settings.plan: %s count failed: %s',
axis, exc)
# MT-8: billing fields are already on g.tenant — no extra DB query needed.
billing_enabled = current_app.config.get('BILLING_ENABLED', False)
+10 -5
View File
@@ -98,17 +98,22 @@
('facilities', 'Active Facilities (total)', plan_info.max_facilities),
] %}
{% for key, label, limit in axes %}
{% set current = quota_usage.get(key, 0) %}
{% set pct = ((current / limit * 100) | int) if limit else 0 %}
{% set over = limit and current >= limit %}
{# None = the counter failed (logged server-side). Rendered as "—"
rather than 0 so an unknown count is never mistaken for real usage. #}
{% set current = quota_usage.get(key) %}
{% set unknown = current is none %}
{% set pct = ((current / limit * 100) | int) if (limit and not unknown) else 0 %}
{% set over = limit and not unknown and current >= limit %}
<div class="mb-3">
<div class="d-flex justify-content-between mb-1" style="font-size:.85rem;">
<span>{{ label }}</span>
<span class="{{ 'text-danger fw-semibold' if over else 'text-muted' }}">
{{ current }}{% if limit %} / {{ limit }}{% else %} / <em>unlimited</em>{% endif %}
{% if unknown %}
<span title="This count could not be read — see the server log.">&mdash;</span>
{% else %}{{ current }}{% endif %}{% if limit %} / {{ limit }}{% else %} / <em>unlimited</em>{% endif %}
</span>
</div>
{% if limit %}
{% if limit and not unknown %}
<div class="progress" style="height:6px;">
<div class="progress-bar {{ 'bg-danger' if over else ('bg-warning' if pct >= 80 else 'bg-success') }}"
role="progressbar" style="width:{{ [pct,100]|min }}%"></div>
+23 -5
View File
@@ -9,7 +9,7 @@ no second control-DB round-trip is needed.
Quota axes:
inspections Inspection.inspection_date in current month, status='completed'
issues Issue.created_at in current month
issues Issue.reported_at in current month
users User.active == True (total, not monthly)
facilities Facility.active == True (total, not monthly)
@@ -18,10 +18,11 @@ all quota checks pass, single-tenant behaviour unchanged.
"""
import logging
from datetime import datetime
from flask import g, current_app
from app.utils.time_utils import now_eastern
logger = logging.getLogger(__name__)
@@ -30,7 +31,15 @@ def _mt_enabled():
def _month_window():
now = datetime.now()
"""[start, end) of the current month, in the timezone the rows are stamped in.
now_eastern(), not datetime.now(): every timestamp in the tenant DB is
written by now_eastern() (rule 2). On a UTC server the two differ by 4-5
hours, so a plain now() puts the month boundary in the wrong place and the
first hours of each month count the wrong rows a discrepancy that only
appears on the 1st and is gone before anyone investigates it.
"""
now = now_eastern()
start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
if now.month == 12:
end = now.replace(year=now.year + 1, month=1, day=1,
@@ -52,11 +61,20 @@ def count_inspections_this_month():
def count_issues_this_month():
"""Issues filed this month.
`reported_at`, NOT `created_at` the issues table has no created_at
column. (IssueComment does, in the same module, which is how the wrong name
got here.) Referencing a missing column raises AttributeError while the
query is built, and every caller wraps this in a try/except, so the failure
was invisible: the plan page silently showed 0 for EVERY axis and the
issues quota was never evaluated at all.
"""
from app.models.issue import Issue
start, end = _month_window()
return (Issue.query
.filter(Issue.created_at >= start,
Issue.created_at < end)
.filter(Issue.reported_at >= start,
Issue.reported_at < end)
.count())