Development

This commit is contained in:
Matthew Grotke 2026-05-26 01:23:05 -04:00
parent 3c7b13e98a
commit 67d951e853
2 changed files with 88 additions and 67 deletions

View file

@ -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.""" """Append cmd to .dashboard-pending if not already present for this cmd+user."""
existing = _read_dashboard_pending() 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: for entry_uuid, entry_ts, entry_cmd, entry_user in existing:
if entry_cmd == cmd and entry_user == current_user: if entry_cmd == cmd and entry_user == current_user:
return entry_uuid, entry_ts 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') 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(): if not _apply_changes_immediately():
return _queue_pending_command(cmd) return _queue_pending_command(cmd, user=user)
done_set = _load_done_set() done_set = _load_done_set()
pending = _read_pending(done_set) pending = _read_pending(done_set)
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 pending: for entry_uuid, entry_ts, entry_cmd, entry_user in pending:
if entry_cmd == cmd and entry_user == current_user: if entry_cmd == cmd and entry_user == current_user:
return entry_uuid, entry_ts return entry_uuid, entry_ts
@ -255,6 +255,18 @@ def _queue_command(cmd):
return entry_uuid, entry_ts 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): def _entry_ts_from_queue(entry_uuid):
try: try:
for line in open(DASHBOARD_QUEUE).read().splitlines(): 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) 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).""" """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'): def queued_msg(cmd=None, description='', action_label='Configuration saved'):

View file

@ -4,7 +4,7 @@ import json, re, subprocess, os, sys, html as html_mod
import sanitize import sanitize
import validation as validate import validation as validate
from datetime import datetime, timezone 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__) bp = Blueprint('view_page', __name__)
@ -1660,16 +1660,18 @@ def render_layout(view_id, content_html, tokens):
if o_user in seen_other_users: if o_user in seen_other_users:
continue continue
seen_other_users.add(o_user) 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: 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' cls = 'info-bar-warning info-bar-running'
else: else:
timing = _format_timing(secs) 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' cls = 'info-bar-warning'
other_bars += f'<div class="info-bar {cls}" data-apply-uuid="{e(o_uuid)}" data-apply-user="{e(o_user)}"><span>{text}</span></div>\n' other_bars += f'<div class="info-bar {cls}" data-apply-uuid="{e(o_uuid)}" data-apply-user="{e(o_user)}"><span>{text}</span></div>\n'
problem_bars = '' problem_bars = ''
if level >= LEVEL_RANK['viewer']:
try: try:
import json as _j import json as _j
st = _j.load(open(f'{CONFIGS_DIR}/.health')) st = _j.load(open(f'{CONFIGS_DIR}/.health'))
@ -1696,8 +1698,14 @@ def render_layout(view_id, content_html, tokens):
grouped.setdefault(item.get('severity', 'error'), []).append(e(detail)) grouped.setdefault(item.get('severity', 'error'), []).append(e(detail))
has_problems = any(items for items in grouped.values()) has_problems = any(items for items in grouped.values())
fix_suffix = '' fix_suffix = ''
fix_uuid = None
if has_problems: if has_problems:
fix_uuid, fix_ts = queue_command('core apply') if level < LEVEL_RANK['administrator']:
fix_suffix = 'Please contact an administrator.'
else:
fix_uuid, fix_ts = _find_cmd_in_queues('fix problems')
if fix_uuid is None:
fix_uuid, fix_ts = queue_command('fix problems', user=current_user)
if _apply_changes_immediately(): if _apply_changes_immediately():
if _is_locked(): if _is_locked():
mtime = _lock_mtime() mtime = _lock_mtime()
@ -1716,7 +1724,8 @@ def render_layout(view_id, content_html, tokens):
problems_list = ('<ul style="margin:0.25em 0;padding-left:1.25em">' problems_list = ('<ul style="margin:0.25em 0;padding-left:1.25em">'
+ ''.join(f'<li>{d}</li>' for d in items) + ''.join(f'<li>{d}</li>' for d in items)
+ '</ul>') + '</ul>')
uuid_attr = f' data-health-uuid="{e(fix_uuid)}"' if _apply_changes_immediately() else '' uuid_attr = (f' data-health-uuid="{e(fix_uuid)}"'
if fix_uuid and _apply_changes_immediately() else '')
fix_html = (f'<div style="margin-top:0.5em"{uuid_attr}>{fix_suffix}</div>' fix_html = (f'<div style="margin-top:0.5em"{uuid_attr}>{fix_suffix}</div>'
if fix_suffix else '') if fix_suffix else '')
content = ('<div style="width:100%">' content = ('<div style="width:100%">'
@ -2610,7 +2619,7 @@ function startPoller(uuid, handlers) {
} }
function startApplyPoller(uuid, bar, mine) { function startApplyPoller(uuid, bar, mine) {
function user() { return bar.getAttribute('data-apply-user') || ''; } function user() { var u = bar.getAttribute('data-apply-user') || ''; return (u === 'unknown' || u === '') ? 'Another user' : u; }
function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); } function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
startPoller(uuid, { startPoller(uuid, {
onPending: function(nextIn) { onPending: function(nextIn) {