Dashboard fixes: staleness, toner, layout

This commit is contained in:
2026-08-11 14:45:29 -04:00
parent 3b37603691
commit 9ae9879b90
7 changed files with 188 additions and 37 deletions

View File

@@ -0,0 +1,38 @@
"""backups: clear the backfilled lastseenat - it was a guess, and it showed.
0002 added lastseenat and backfilled it from collectedat, reasoning that the
last change was the last moment the config could be PROVEN current. On a fleet
that was wrong in practice: an unchanged config writes no revision, so a machine
whose settings last changed nine months ago got a nine-month-old lastseenat and
was immediately reported as a stopped backup. Every chain lit up at once, which
is worse than no card - it says the fleet is broken when it is fine.
The honest value is NULL: before this column existed, nothing recorded when a
config was last confirmed, and inventing a date does not change that. A chain
becomes measurable the first time its PC posts after the upgrade, which for
NTLARS is within a day.
So staleness now IGNORES a chain whose lastseenat is NULL, rather than falling
back to timestamps that mean something else.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'backups0003clearlastseen'
down_revision = 'backups0002lastseenat'
branch_labels = None
depends_on = None
def upgrade():
columns = {c['name'] for c in
sa.inspect(op.get_bind()).get_columns('backuprevisions')}
if 'lastseenat' in columns:
op.execute('UPDATE backuprevisions SET lastseenat = NULL')
def downgrade():
# Nothing to restore: the backfilled values were derived, not recorded.
pass

View File

@@ -65,10 +65,14 @@ def stalechains(days=None, limit=50):
rows = []
for revision in latestperchain().values():
# Rows written before lastseenat existed fall back to the timestamps
# that do exist, so an old install reports something sane on day one
# rather than every chain at once.
seen = revision.lastseenat or revision.collectedat or revision.createdat
# NULL means never confirmed since the column existed, and it is NOT
# substituted with collectedat. That substitution is what the first
# version did, and on a real fleet it reported every machine whose
# config had simply been stable for months as a stopped backup - the
# whole board lit up and said the site was broken when it was fine.
# A chain becomes measurable the first time its PC posts; until then
# this card says nothing about it, which is the truth.
seen = revision.lastseenat
if seen is None or seen >= cutoff:
continue
asset = db.session.get(Asset, revision.assetid)

View File

@@ -1395,6 +1395,18 @@ def delete_model_supply(modelsupplyid: int):
return success_response(message='Supply deleted')
def _shortsupplyname(name):
"""'Black Toner Level' -> 'Black'. The card has one line per printer, and
the words Toner and Level carry no information when every row is a toner
level."""
text = (name or 'supply').strip()
for noise in (' Cartridge Level', ' Toner Level', ' Level', ' Cartridge'):
if text.endswith(noise):
text = text[:-len(noise)]
break
return text or 'supply'
@printers_asset_bp.route('/dashboard/supplies', methods=['GET'])
@jwt_required()
@require_permission('printers.view')
@@ -1426,9 +1438,11 @@ def dashboard_supplies():
'printername': printer['printername'] or printer['assetnumber'],
'location': printer['location'],
'status': 'critical' if criticals else 'low',
# 'remaining' is the percent left. There is no 'percent' key -
# reading one rendered every cartridge as "None%" on the board.
'supplies': ', '.join(
'{} {}%'.format(s.get('name') or s.get('type') or 'supply',
s.get('percent'))
'{} {}%'.format(_shortsupplyname(s.get('name')),
s.get('remaining'))
for s in worst),
'iscritical': bool(criticals),
})