# 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.device_registration import DeviceRegistration 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 = ( DeviceRegistration.query .order_by(DeviceRegistration.last_seen_at.desc()) .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. Form fields ----------- current_version str The version string to treat as current (e.g. "1.3.0") message str Optional custom message body (default provided) """ 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): """Convert "1.3.0" → (1, 3, 0) for comparison. Non-numeric parts → 0.""" try: return tuple(int(x) for x in v.strip().split('.')) except ValueError: return (0,) target_v = version_tuple(current_version) # Find all devices running an older version all_devices = DeviceRegistration.query.all() outdated = [d for d in all_devices if version_tuple(d.app_version) < target_v] if not outdated: flash(f'No devices found running a version older than {current_version}.', 'info') return redirect(url_for('devices.index')) # Deduplicate by user_id — one notification per user even if they have # multiple devices registered (e.g. primary + secondary server devices). 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 | devices_outdated=%d | by=%s', current_version, notified, len(outdated), 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'))