# app/routes/devices.py # ---------------------- # Admin-only page for viewing registered iOS devices and pushing # "please update" notifications to users on outdated app versions. import logging from flask import Blueprint, render_template, request, redirect, url_for, flash from flask_login import login_required, current_user from app import db from app.models.api_token import DeviceToken from app.models.notification import Notification from app.utils.decorators import admin_required from app.utils.audit import log_action, ACTION_CREATE logger = logging.getLogger(__name__) bp = Blueprint('devices', __name__, url_prefix='/admin/devices') @bp.route('/', methods=['GET']) @login_required @admin_required def index(): """Show all registered devices, most-recently-seen first.""" devices = ( DeviceToken.query .order_by(DeviceToken.last_seen_at.desc().nullslast()) .all() ) return render_template('admin/devices.html', devices=devices) @bp.route('/notify', methods=['POST']) @login_required @admin_required def notify_update(): """ Send an in-app update notice to all users whose app_version is below the version string entered by the admin. """ current_version = (request.form.get('current_version') or '').strip() custom_message = (request.form.get('message') or '').strip() if not current_version: flash('Current version is required.', 'danger') return redirect(url_for('devices.index')) def version_tuple(v: str): try: return tuple(int(x) for x in v.strip().split('.')) except ValueError: return (0,) target_v = version_tuple(current_version) all_devices = DeviceToken.query.all() outdated = [d for d in all_devices if version_tuple(d.app_version or '0') < target_v] if not outdated: flash(f'No devices found running a version older than {current_version}.', 'info') return redirect(url_for('devices.index')) seen_users = set() notified = 0 for device in outdated: if device.user_id in seen_users: continue seen_users.add(device.user_id) body = custom_message or ( f'A new version of JanitorialQC ({current_version}) is available. ' f'Please update from the App Store to get the latest features and fixes.' ) notif = Notification( user_id = device.user_id, title = f'App Update Available — v{current_version}', body = body, link = None, event_type = 'admin_broadcast', is_read = False, ) db.session.add(notif) notified += 1 db.session.commit() log_action(ACTION_CREATE, 'DeviceUpdateNotice', 0, f'v{current_version} notice → {notified} user(s)', f'outdated_devices={len(outdated)}; sent_by={current_user.username}') logger.info('DEVICES | update_notice | version=%s | users_notified=%d | by=%s', current_version, notified, current_user.username) flash( f'Update notice sent to {notified} user(s) on {len(outdated)} outdated device(s).', 'success' ) return redirect(url_for('devices.index'))