diff --git a/docker/routlin-dash/app/action_actions.py b/docker/routlin-dash/app/action_actions.py index c428abb..fd4375c 100644 --- a/docker/routlin-dash/app/action_actions.py +++ b/docker/routlin-dash/app/action_actions.py @@ -1,8 +1,8 @@ from flask import Blueprint, request, redirect, flash, session from auth import require_level -from config_utils import (flush_selected_to_queue, delete_pending_by_uuids, - get_dashboard_pending, _is_locked, _format_timing, - _seconds_until_next_run) +from config_utils import (flush_pending_to_queue, flush_selected_to_queue, + delete_pending_by_uuids, get_dashboard_pending, + _is_locked, _format_timing, _seconds_until_next_run) bp = Blueprint('action_actions', __name__) @@ -20,11 +20,10 @@ def actions_cardoptions_save(): @bp.route('/action/actions_cardpendingchanges_applynow', methods=['POST']) @require_level('administrator') def actions_cardpendingchanges_applynow(): - selected_uuids = request.form.getlist('selected_uuids') - if not selected_uuids: - flash('No items selected.', 'info') + if not get_dashboard_pending(): + flash('No pending changes to apply.', 'info') return redirect(_VIEW) - flush_selected_to_queue(selected_uuids) + flush_pending_to_queue() if _is_locked(): msg = 'Changes queued. They are being applied now.' else: diff --git a/docker/routlin-dash/app/config_utils.py b/docker/routlin-dash/app/config_utils.py index f3ae4a5..06ec600 100644 --- a/docker/routlin-dash/app/config_utils.py +++ b/docker/routlin-dash/app/config_utils.py @@ -144,6 +144,28 @@ def get_dashboard_pending(): return _read_dashboard_pending() +def get_dashboard_done(): + """Return list of (uuid, applied_ts) from .dashboard-done, newest first.""" + items = [] + try: + lines = open(DASHBOARD_DONE).read().splitlines() + except Exception: + return items + for line in lines: + if not line.strip(): + continue + try: + parts = line.split(None, 1) + if len(parts) >= 2: + items.append((parts[0], int(parts[1]))) + elif len(parts) == 1: + items.append((parts[0], None)) + except Exception: + pass + items.reverse() + return items + + def flush_pending_to_queue(): """Move all entries from .dashboard-pending to .dashboard-queue and clear pending.""" items = _read_dashboard_pending() @@ -325,6 +347,64 @@ def _find_snapshot_dependencies(path, key): return [] +def _items_match(item, ref): + """Return True if item and ref refer to the same entity by a common identifier field.""" + if not isinstance(item, dict) or not isinstance(ref, dict): + return item == ref + for field in ('ip', 'name', 'mac_address', 'host', 'id', 'address'): + if field in ref and field in item: + return item[field] == ref[field] + return item == ref + + +def revert_snapshot_to_core(entry_uuid): + """Apply the inverse of a snapshot to core.json and queue a new pending change. + + Returns (flash_message, success_bool). + """ + snap = load_snapshot_for_uuid(entry_uuid) + if not snap: + return f'Snapshot not found for {entry_uuid[:8]}.', False + + path = snap['path'] + key = snap['key'] + before = snap['before'] # original state to restore + after = snap['after'] # applied state to undo + operation = snap['operation'] + + if operation == 'revert': + return 'This change is already a revert; cannot revert again.', False + + core = load_core() + + if key == 'global': + if before is None: + core.pop(path, None) + else: + core[path] = before + else: + items = core.setdefault(path, []) + if operation == 'add': + core[path] = [x for x in items if not _items_match(x, after)] + elif operation == 'delete': + if before: + core[path].append(before) + else: + if before and after: + for i, item in enumerate(items): + if _items_match(item, after): + items[i] = before + break + + msg = save_core_with_snapshot( + core, path=path, key=key, operation='revert', + before=after, after=before, + description=f"Reverted: {snap.get('description', '')}", + cmd=snap.get('cmd', 'core apply'), + ) + return msg or 'Reverted.', True + + def load_snapshot_for_uuid(entry_uuid): """Return the snapshot dict for the given UUID, or None if not found.""" try: diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 2b999fe..8fe2710 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 core_hash, get_pending_entries, get_dashboard_pending, 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 core_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 bp = Blueprint('view_page', __name__) @@ -594,7 +594,7 @@ def collect_tokens(): pending_items = get_dashboard_pending() if pending_items: rows = '' - for _uuid, ts, cmd, user in pending_items: + for _uuid, ts, _cmd, user in pending_items: snap = load_snapshot_for_uuid(_uuid) dt_str = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M') snap_desc = e(snap['description']) if snap else '' @@ -604,7 +604,6 @@ def collect_tokens(): rows += (f'
| Applied | ' + 'Description | ' + 'Before | ' + 'After | ' + 'Snapshot | ' + 'User | ' + '
|---|