diff --git a/docker/routlin-dash/app/action_actions.py b/docker/routlin-dash/app/action_actions.py index 39e7163..b3b68bf 100644 --- a/docker/routlin-dash/app/action_actions.py +++ b/docker/routlin-dash/app/action_actions.py @@ -2,7 +2,7 @@ from flask import Blueprint, request, redirect, flash, session from auth import require_level from config_utils import (flush_pending_to_queue, get_dashboard_pending, revert_snapshot_to_config, queued_msg, - _is_locked, _format_timing, _seconds_until_next_run) + _timing_status_msg) bp = Blueprint('action_actions', __name__) @@ -24,22 +24,7 @@ def actions_cardpending_applynow(): flash('No pending changes to apply.', 'info') return redirect(_VIEW) flush_pending_to_queue() - if _is_locked(): - msg = 'Changes queued. They are being applied now.' - else: - timing = _format_timing(_seconds_until_next_run()) - if timing: - msg = f'Changes queued. They will be applied {timing}.' - else: - msg = 'Changes queued. The processing service is not running.' - flash(msg, 'success') - return redirect(_VIEW) - - -@bp.route('/action/queue_core_apply', methods=['POST']) -@require_level('administrator') -def queue_core_apply(): - flash(queued_msg('core apply', action_label='Apply queued'), 'success') + flash(_timing_status_msg(None, 'Changes queued'), 'success') return redirect(_VIEW) diff --git a/docker/routlin-dash/app/config_utils.py b/docker/routlin-dash/app/config_utils.py index bc420f6..cb64e17 100644 --- a/docker/routlin-dash/app/config_utils.py +++ b/docker/routlin-dash/app/config_utils.py @@ -255,26 +255,23 @@ def _lock_mtime(): return None -def _build_timing_msg(entry_ts, cmd, action_label='Configuration saved'): - if not _apply_changes_immediately(): - return f'{action_label}. Click Apply Now on the Configuration Changes card to apply.' +def _timing_status_msg(entry_ts, action_label): + """Return a flash message for a command already written to the queue.""" if _is_locked(): mtime = _lock_mtime() if entry_ts is not None and mtime and entry_ts < mtime: - return f'{action_label}. Changes are being applied now.' - return f'{action_label}. Changes will be applied on the next run.' + return f'{action_label}. Your changes are being applied now...' + return f'{action_label}. Your changes will be applied on the next run.' timing = _format_timing(_seconds_until_next_run()) if timing: - return f'{action_label}. Changes will be applied {timing}.' - if cmd is None: - return f'{action_label}. The processing service is not running.' - parts = cmd.split() - cli_cmd = f'sudo python3 {parts[0]}.py --{parts[1]}' if len(parts) == 2 else cmd - install_cmd = 'sudo python3 install.py' - from markupsafe import Markup - return Markup(f'{action_label}. The command processing service is not installed. ' - f'Run {install_cmd} to enable it, ' - f'or {cli_cmd} to apply manually.') + return f'{action_label}. Your changes will be applied {timing}.' + return f'{action_label}. The processing service is not running.' + + +def _build_timing_msg(entry_ts, action_label='Configuration saved'): + if not _apply_changes_immediately(): + return f'{action_label}. Visit Actions page to apply your changes.' + return _timing_status_msg(entry_ts, action_label) def queue_command(cmd, description=''): @@ -287,7 +284,7 @@ def queued_msg(cmd=None, description='', action_label='Configuration saved'): entry_ts = None if cmd is not None: _entry_uuid, entry_ts = queue_command(cmd) - return _build_timing_msg(entry_ts, cmd, action_label) + return _build_timing_msg(entry_ts, action_label) # Snapshot system =================================================== @@ -434,7 +431,7 @@ def save_config_with_snapshot(new_config, path, key, operation, before, after, else: _queue_pending_presigned(cmd, entry_uuid, entry_ts) - return _build_timing_msg(entry_ts, cmd) + return _build_timing_msg(entry_ts) # Misc ============================================================== diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 0af3419..b21d884 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -4,7 +4,7 @@ import json, re, subprocess, os, sys, html as html_mod import sanitize import validation as validate from datetime import datetime, timezone -from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR +from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, queue_command, _apply_changes_immediately, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR bp = Blueprint('view_page', __name__) @@ -1649,7 +1649,7 @@ def render_layout(view_id, content_html, tokens): cls = 'info-bar-warning info-bar-running' else: timing = _format_timing(secs) - text = f'{e(o_user)} has pending changes which will be applied {timing}.' if timing else f'{e(o_user)} has pending changes which will be applied on the next timer tick.' + text = f'{e(o_user)} has pending changes which will be applied {timing}.' if timing else f'{e(o_user)} has pending changes. The processing service is not running.' cls = 'info-bar-warning' other_bars += f'
{text}
\n' @@ -1663,56 +1663,45 @@ def render_layout(view_id, content_html, tokens): if item.get('status') == 'problem': sev = item.get('severity', 'error') text = e(item.get('detail', item.get('name', ''))) - tip = item.get('suggestion', '') - grouped.setdefault(sev, []).append((text, tip)) + grouped.setdefault(sev, []).append(text) for item in st.get('services', []): if item.get('status') == 'problem': name = item.get('name', '') utype = 'timer' if name.endswith('.timer') else 'service' if name.endswith('.service') else 'unit' - exp_parts, act_parts, fix_parts = [], [], [] + exp_parts, act_parts = [], [] if not item.get('active_ok'): exp_parts.append(item.get('expected_active', 'active')) act_parts.append(item.get('active', 'unknown')) - fix_parts.append('activate') if not item.get('enabled_ok'): exp_parts.append(item.get('expected_enabled', 'enabled')) act_parts.append(item.get('enabled', 'unknown')) - fix_parts.append('enable') detail = (f"The {utype} `{name}` is expected to be " f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.") - tip = f"Run `sudo python3 core.py --apply` to {' and '.join(reversed(fix_parts))} it." - sev = item.get('severity', 'error') - grouped.setdefault(sev, []).append((e(detail), tip)) + grouped.setdefault(item.get('severity', 'error'), []).append(e(detail)) + has_problems = any(items for items in grouped.values()) + fix_suffix = '' + if has_problems: + fix_uuid, fix_ts = queue_command('core apply') + if _apply_changes_immediately(): + if _is_locked(): + mtime = _lock_mtime() + fix_suffix = ('Fix is being applied now...' if fix_ts and mtime and fix_ts < mtime + else 'Fix will be applied on the next run.') + else: + timing = _format_timing(_seconds_until_next_run()) + fix_suffix = (f'Fix will be applied {timing}.' if timing + else 'Fix pending. The processing service is not running.') + else: + fix_suffix = 'Fix pending. Visit Actions page ASAP to apply fix.' for sev, items in grouped.items(): if not items: continue cls = 'info-bar-danger' if sev == 'error' else 'info-bar-warning' - seen_cmds, fix_cmds = set(), [] - for _, tip in items: - if tip: - m = re.search(r'`([^`]+)`', tip) - cmd = m.group(1) if m else tip - if cmd not in seen_cmds: - seen_cmds.add(cmd) - fix_cmds.append(cmd) problems_list = ('') - fix_html = '' - if fix_cmds: - _queue_urls = { - 'sudo python3 core.py --apply': '/action/queue_core_apply', - } - def _fix_item(c): - url = _queue_urls.get(c) - if url: - btn = (f'
' - f'
') - return f'
  • {btn} or manually run {e(c)}
  • ' - return f'
  • Run {e(c)}
  • ' - fix_items = ''.join(_fix_item(c) for c in fix_cmds) - fix_html = ('
    To fix:
    ' - f'') + fix_html = (f'
    {e(fix_suffix)}
    ' + if fix_suffix else '') content = ('
    ' '
    Health check - problems found:
    ' + problems_list + fix_html @@ -2555,44 +2544,27 @@ var validateEl; }); })(); -function startApplyPoller(uuid, bar, mine) { +function timingPhrase(n) { + return n <= 5 ? 'momentarily' + : n < 60 ? 'in about ' + n + ' seconds' + : 'in about ' + Math.round(n / 60) + ' minute' + (Math.round(n / 60) !== 1 ? 's' : ''); +} + +function startPoller(uuid, handlers) { var nextIn = null; var pollTimer = null; var tickTimer = null; - function user() { return bar.getAttribute('data-apply-user') || ''; } - function esc(s) { return s.replace(/&/g,'&').replace(//g,'>'); } - function setHtml(html) { bar.innerHTML = html; } - - function updateCountdown() { - if (nextIn === null) { - setHtml(mine ? 'Configuration saved. The command processing service is not installed. Run core.py --install to enable it, or core.py --apply to apply manually.' - : esc(user()) + ' has pending changes. The command processing service is not installed.'); - return; - } - var timing = nextIn <= 5 ? 'momentarily' - : nextIn < 60 ? 'in about ' + nextIn + ' seconds' - : 'in about ' + Math.round(nextIn / 60) + ' minute' + (Math.round(nextIn / 60) !== 1 ? 's' : ''); - setHtml(mine ? 'Configuration saved. Changes will be applied ' + timing + '.' - : esc(user()) + ' has pending changes which will be applied ' + timing + '.'); - } - function onStatus(data) { if (data.status === 'complete') { - bar.classList.remove('info-bar-running'); - setHtml(mine ? 'Changes have been applied.' : esc(user()) + '\'s changes have been applied.'); - clearTimeout(pollTimer); - clearTimeout(tickTimer); - return; + clearTimeout(pollTimer); clearTimeout(tickTimer); + handlers.onComplete(); return; } if (data.status === 'running') { - bar.classList.add('info-bar-running'); - setHtml(mine ? 'Configuration saved. Changes are being applied now...' - : esc(user()) + '\'s changes are being applied now...'); + handlers.onRunning(); } else { - bar.classList.remove('info-bar-running'); if (data.next_in !== null && data.next_in !== undefined) { nextIn = data.next_in; } - updateCountdown(); + handlers.onPending(nextIn); } pollTimer = setTimeout(doPoll, 3000); } @@ -2605,7 +2577,7 @@ function startApplyPoller(uuid, bar, mine) { } function tick() { - if (nextIn !== null && nextIn > 0) { nextIn--; updateCountdown(); } + if (nextIn !== null && nextIn > 0) { nextIn--; handlers.onPending(nextIn); } tickTimer = setTimeout(tick, 1000); } @@ -2613,6 +2585,31 @@ function startApplyPoller(uuid, bar, mine) { tick(); } +function startApplyPoller(uuid, bar, mine) { + function user() { return bar.getAttribute('data-apply-user') || ''; } + function esc(s) { return s.replace(/&/g,'&').replace(//g,'>'); } + startPoller(uuid, { + onPending: function(nextIn) { + bar.classList.remove('info-bar-running'); + bar.innerHTML = nextIn === null + ? (mine ? 'Configuration saved. The processing service is not running.' + : esc(user()) + ' has pending changes. The processing service is not running.') + : (mine ? 'Configuration saved. Your changes will be applied ' + timingPhrase(nextIn) + '.' + : esc(user()) + ' has pending changes which will be applied ' + timingPhrase(nextIn) + '.'); + }, + onRunning: function() { + bar.classList.add('info-bar-running'); + bar.innerHTML = mine ? 'Configuration saved. Your changes are being applied now...' + : esc(user()) + '\'s changes are being applied now...'; + }, + onComplete: function() { + bar.classList.remove('info-bar-running'); + bar.innerHTML = mine ? 'Your changes have been applied.' + : esc(user()) + '\'s changes have been applied.'; + } + }); +} + (function() { if (typeof APPLY_UUID !== 'undefined' && APPLY_UUID) { var bar = document.querySelector('.info-bar-flash.info-bar-success'); @@ -2621,6 +2618,16 @@ function startApplyPoller(uuid, bar, mine) { document.querySelectorAll('[data-apply-uuid]').forEach(function(bar) { startApplyPoller(bar.getAttribute('data-apply-uuid'), bar, false); }); + document.querySelectorAll('[data-health-uuid]').forEach(function(el) { + startPoller(el.getAttribute('data-health-uuid'), { + onPending: function(nextIn) { + el.textContent = nextIn === null ? 'Fix pending. The processing service is not running.' + : 'Fix will be applied ' + timingPhrase(nextIn) + '.'; + }, + onRunning: function() { el.textContent = 'Fix is being applied now...'; }, + onComplete: function() { el.textContent = 'Fix has been applied.'; } + }); + }); })(); (function() {