diff --git a/docker/routlin-dash/app/config_utils.py b/docker/routlin-dash/app/config_utils.py index 1604b42..d0f0eec 100644 --- a/docker/routlin-dash/app/config_utils.py +++ b/docker/routlin-dash/app/config_utils.py @@ -217,10 +217,10 @@ def flush_pending_to_queue(): -def _queue_pending_command(cmd): +def _queue_pending_command(cmd, user=None): """Append cmd to .dashboard-pending if not already present for this cmd+user.""" existing = _read_dashboard_pending() - current_user = session.get('email_address', 'unknown') + current_user = user or session.get('email_address', 'unknown') for entry_uuid, entry_ts, entry_cmd, entry_user in existing: if entry_cmd == cmd and entry_user == current_user: return entry_uuid, entry_ts @@ -238,12 +238,12 @@ def _queue_pending_presigned(cmd, entry_uuid, entry_ts): f.write(f'{entry_uuid} {entry_ts} [{cmd}] ({current_user})\n') -def _queue_command(cmd): +def _queue_command(cmd, user=None): if not _apply_changes_immediately(): - return _queue_pending_command(cmd) - done_set = _load_done_set() - pending = _read_pending(done_set) - current_user = session.get('email_address', 'unknown') + return _queue_pending_command(cmd, user=user) + done_set = _load_done_set() + pending = _read_pending(done_set) + current_user = user or session.get('email_address', 'unknown') for entry_uuid, entry_ts, entry_cmd, entry_user in pending: if entry_cmd == cmd and entry_user == current_user: return entry_uuid, entry_ts @@ -255,6 +255,18 @@ def _queue_command(cmd): return entry_uuid, entry_ts +def _find_cmd_in_queues(cmd): + """Return (uuid, ts) of first matching entry in .dashboard-pending or .dashboard-queue, or (None, None).""" + for entry_uuid, entry_ts, entry_cmd, entry_user in _read_dashboard_pending(): + if entry_cmd == cmd: + return entry_uuid, entry_ts + done_set = _load_done_set() + for entry_uuid, entry_ts, entry_cmd, entry_user in _read_pending(done_set): + if entry_cmd == cmd: + return entry_uuid, entry_ts + return None, None + + def _entry_ts_from_queue(entry_uuid): try: for line in open(DASHBOARD_QUEUE).read().splitlines(): @@ -309,9 +321,9 @@ def _build_timing_msg(entry_ts, action_label='Configuration saved'): return _timing_status_msg(entry_ts, action_label) -def queue_command(cmd, description=''): +def queue_command(cmd, description='', user=None): """Queue a command without generating a flash message. description is ignored (kept for compat).""" - return _queue_command(cmd) + return _queue_command(cmd, user=user) def queued_msg(cmd=None, description='', action_label='Configuration saved'): diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 4f1bc2c..3cf2e57 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, load_all_snapshots, get_done_timestamps, queue_command, _apply_changes_immediately, _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, load_all_snapshots, get_done_timestamps, queue_command, _find_cmd_in_queues, _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__) @@ -1660,72 +1660,81 @@ def render_layout(view_id, content_html, tokens): if o_user in seen_other_users: continue seen_other_users.add(o_user) + _display_user = 'Another user' if o_user in ('unknown', '') else e(o_user) if locked and lock_mtime and o_ts < lock_mtime: - text = f'{e(o_user)}\'s changes are being applied now...' + text = f'{_display_user}\'s changes are being applied now...' 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. The processing service is not running.' + text = f'{_display_user} has pending changes which will be applied {timing}.' if timing else f'{_display_user} has pending changes. The processing service is not running.' cls = 'info-bar-warning' other_bars += f'
\n' problem_bars = '' - try: - import json as _j - st = _j.load(open(f'{CONFIGS_DIR}/.health')) - grouped = {'error': [], 'warning': []} - for section in ('configurations', 'logs'): - for item in st.get(section, []): + if level >= LEVEL_RANK['viewer']: + try: + import json as _j + st = _j.load(open(f'{CONFIGS_DIR}/.health')) + grouped = {'error': [], 'warning': []} + for section in ('configurations', 'logs'): + for item in st.get(section, []): + if item.get('status') == 'problem': + sev = item.get('severity', 'error') + text = e(item.get('detail', item.get('name', ''))) + grouped.setdefault(sev, []).append(text) + for item in st.get('services', []): if item.get('status') == 'problem': - sev = item.get('severity', 'error') - text = e(item.get('detail', item.get('name', ''))) - 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 = [], [] - if not item.get('active_ok'): - exp_parts.append(item.get('expected_active', 'active')) - act_parts.append(item.get('active', 'unknown')) - if not item.get('enabled_ok'): - exp_parts.append(item.get('expected_enabled', 'enabled')) - act_parts.append(item.get('enabled', 'unknown')) - detail = (f"The {utype} `{name}` is expected to be " - f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.") - 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.') + name = item.get('name', '') + utype = 'timer' if name.endswith('.timer') else 'service' if name.endswith('.service') else 'unit' + 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')) + if not item.get('enabled_ok'): + exp_parts.append(item.get('expected_enabled', 'enabled')) + act_parts.append(item.get('enabled', 'unknown')) + detail = (f"The {utype} `{name}` is expected to be " + f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.") + grouped.setdefault(item.get('severity', 'error'), []).append(e(detail)) + has_problems = any(items for items in grouped.values()) + fix_suffix = '' + fix_uuid = None + if has_problems: + if level < LEVEL_RANK['administrator']: + fix_suffix = 'Please contact an administrator.' 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 the 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' - problems_list = ('