Development
This commit is contained in:
parent
adcfe55c7c
commit
59d3d65d18
7 changed files with 146 additions and 75 deletions
|
|
@ -52,6 +52,7 @@ def ddns_cardaddaccount_add():
|
|||
before=None, after=copy.deepcopy(entry),
|
||||
description=f'Added DDNS provider: {description}',
|
||||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
|
@ -103,6 +104,7 @@ def ddns_tableaccounts_rowedit():
|
|||
before=before, after=copy.deepcopy(entry),
|
||||
description=f'Edited DDNS provider: {description}',
|
||||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
|
@ -134,6 +136,7 @@ def ddns_tableaccounts_rowdelete():
|
|||
before=before, after=None,
|
||||
description=f'Deleted DDNS provider: {description}',
|
||||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
|
@ -191,6 +194,7 @@ def ddns_cardipcheckservices_save():
|
|||
before=before, after=copy.deepcopy(services),
|
||||
description='Updated DDNS IP check services',
|
||||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
|
@ -219,6 +223,7 @@ def ddns_cardlogging_save():
|
|||
before=before, after=copy.deepcopy(cfg['ddns']['general']),
|
||||
description='Updated DDNS logging settings',
|
||||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import copy
|
||||
import re
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
from auth import require_level
|
||||
from config_utils import load_config, save_config, verify_config_hash, queued_msg
|
||||
from config_utils import load_config, save_config_with_snapshot, verify_config_hash, queued_msg
|
||||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
|
|
@ -62,21 +63,26 @@ def dnsblocking_tableblocklists_rowdelete():
|
|||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
cfg = load_config()
|
||||
items = cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
name = before.get('name', str(idx))
|
||||
items.pop(idx)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
save_config(cfg)
|
||||
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key=name, operation='delete',
|
||||
before=before, after=None,
|
||||
description=f'Deleted blocklist: {name}',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
||||
|
|
@ -95,12 +101,13 @@ def dnsblocking_tableblocklists_rowedit():
|
|||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
cfg = load_config()
|
||||
items = cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
items[idx].update({
|
||||
'name': fields['name'],
|
||||
'description': fields['description'],
|
||||
|
|
@ -112,9 +119,12 @@ def dnsblocking_tableblocklists_rowedit():
|
|||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
save_config(cfg)
|
||||
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key=fields['name'], operation='edit',
|
||||
before=before, after=copy.deepcopy(items[idx]),
|
||||
description=f'Edited blocklist: {fields["name"]}',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
||||
|
|
@ -128,28 +138,32 @@ def dnsblocking_cardaddblocklist_add():
|
|||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
cfg = load_config()
|
||||
blocklists = cfg.setdefault('dns_blocking', {}).setdefault('blocklists', [])
|
||||
|
||||
if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists):
|
||||
flash('The configuration has not been saved because a blocklist with that name already exists.', 'error')
|
||||
return redirect(VIEW)
|
||||
|
||||
blocklists.append({
|
||||
entry = {
|
||||
'name': fields['name'],
|
||||
'description': fields['description'],
|
||||
'format': fields['format'],
|
||||
'url': fields['url'],
|
||||
'save_as': _save_as_from_name(fields['name']),
|
||||
})
|
||||
}
|
||||
blocklists.append(entry)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
save_config(cfg)
|
||||
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key=fields['name'], operation='add',
|
||||
before=None, after=copy.deepcopy(entry),
|
||||
description=f'Added blocklist: {fields["name"]}',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
||||
|
|
@ -166,11 +180,15 @@ def dnsblocking_cardblocklistrefresh_save():
|
|||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('dns_blocking', {}).get('general', {}))
|
||||
cfg.setdefault('dns_blocking', {}).setdefault('general', {})['daily_execute_time_24hr_local'] = daily_execute_time
|
||||
save_config(cfg)
|
||||
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key='general', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['dns_blocking']['general']),
|
||||
description='Updated daily blocklist refresh time',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
||||
|
||||
|
|
@ -196,7 +214,8 @@ def dnsblocking_cardlogging_save():
|
|||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('dns_blocking', {}).get('general', {}))
|
||||
cfg.setdefault('dns_blocking', {}).setdefault('general', {}).update({
|
||||
'log_max_kb': log_max_kb,
|
||||
'log_errors_only': log_errors_only,
|
||||
|
|
@ -206,7 +225,10 @@ def dnsblocking_cardlogging_save():
|
|||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
save_config(cfg)
|
||||
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key='general', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['dns_blocking']['general']),
|
||||
description='Updated DNS blocking log settings',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import copy
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
from auth import require_level
|
||||
from config_utils import load_config, save_config, verify_config_hash, queued_msg, queue_command
|
||||
from config_utils import load_config, save_config_with_snapshot, verify_config_hash, queued_msg, queue_command
|
||||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
|
|
@ -54,8 +55,9 @@ def networkinterfaces_cardnetworkinterface_save():
|
|||
flash(f"Interface '{iface}' does not exist on this system.", 'error')
|
||||
return redirect(_VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
gen = cfg.setdefault('network_interfaces', {})
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('network_interfaces', {}))
|
||||
gen = cfg.setdefault('network_interfaces', {})
|
||||
gen['wan_interface'] = wan
|
||||
gen['lan_interface'] = lan
|
||||
errors = validate.validate_config(cfg)
|
||||
|
|
@ -63,9 +65,12 @@ def networkinterfaces_cardnetworkinterface_save():
|
|||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
save_config(cfg)
|
||||
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='network_interfaces', key='global', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['network_interfaces']),
|
||||
description='Updated network interfaces',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
from auth import require_level
|
||||
from config_utils import load_config, save_config, verify_config_hash, queued_msg
|
||||
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ def upstreamdns_cardupstreamdns_save():
|
|||
return redirect(_VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('upstream_dns', {}))
|
||||
current = cfg.get('upstream_dns', {})
|
||||
if (strict_order == bool(current.get('strict_order', False)) and
|
||||
upstream_servers == current.get('upstream_servers', [])):
|
||||
|
|
@ -48,8 +50,12 @@ def upstreamdns_cardupstreamdns_save():
|
|||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
save_config(cfg)
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='upstream_dns', key='global', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['upstream_dns']),
|
||||
description='Updated upstream DNS servers',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
|
||||
|
||||
|
|
@ -66,6 +72,7 @@ def upstreamdns_cardforwardingdnsservice_save():
|
|||
return redirect(_VIEW)
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('upstream_dns', {}))
|
||||
current = cfg.get('upstream_dns', {})
|
||||
if cache_size == int(current.get('cache_size', 0)):
|
||||
flash('No changes detected.', 'info')
|
||||
|
|
@ -77,6 +84,10 @@ def upstreamdns_cardforwardingdnsservice_save():
|
|||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
save_config(cfg)
|
||||
flash(queued_msg('core apply'), 'success')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='upstream_dns', key='global', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['upstream_dns']),
|
||||
description='Updated DNS cache size',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
|
|
|
|||
|
|
@ -421,8 +421,10 @@ def save_config_with_snapshot(new_core, path, key, operation, before, after,
|
|||
description='', cmd='core apply', queue=True):
|
||||
"""
|
||||
Write a .snapshots/{ts}-{uuid}.json file, save new_core to disk, and
|
||||
optionally create a pending queue entry. Returns a flash message string
|
||||
when queue=True, otherwise None.
|
||||
optionally create a pending queue entry. Returns a flash message string.
|
||||
|
||||
queue=False: skips queueing and records the change directly in
|
||||
.dashboard-done so it appears in Change History without a pending step.
|
||||
"""
|
||||
entry_uuid = str(uuid.uuid4())
|
||||
entry_ts = int(datetime.now().timestamp())
|
||||
|
|
@ -450,7 +452,9 @@ def save_config_with_snapshot(new_core, path, key, operation, before, after,
|
|||
save_config(new_core)
|
||||
|
||||
if not queue:
|
||||
return None
|
||||
with open(DASHBOARD_DONE, 'a') as f:
|
||||
f.write(f'{entry_uuid} {entry_ts}\n')
|
||||
return 'Saved.'
|
||||
|
||||
if _apply_changes_immediately():
|
||||
with open(DASHBOARD_QUEUE, 'a') as f:
|
||||
|
|
|
|||
|
|
@ -594,22 +594,27 @@ def collect_tokens():
|
|||
pending_items = get_dashboard_pending()
|
||||
if pending_items:
|
||||
rows = ''
|
||||
_tr_onclick = (
|
||||
'onclick="if(event.target.type!==\'checkbox\')'
|
||||
'this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
||||
)
|
||||
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 ''
|
||||
before_html = _render_snap_val(snap.get('before') if snap else None)
|
||||
after_html = _render_snap_val(snap.get('after') if snap else None)
|
||||
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 ''
|
||||
before_val = snap.get('before') if snap else None
|
||||
after_val = snap.get('after') if snap else None
|
||||
snap_id = e(_uuid[:8]) if snap else ''
|
||||
rows += (f'<tr>'
|
||||
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(_uuid)}"/></td>'
|
||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
||||
f'<td class="table-cell">{snap_desc}</td>'
|
||||
f'<td class="table-cell">{before_html}</td>'
|
||||
f'<td class="table-cell">{after_html}</td>'
|
||||
f'<td class="table-cell">{snap_id}</td>'
|
||||
f'<td class="table-cell">{e(user)}</td>'
|
||||
f'</tr>')
|
||||
rows += (f'<tr style="cursor:pointer" {_tr_onclick}>'
|
||||
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(_uuid)}"/></td>'
|
||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
||||
f'<td class="table-cell">{snap_desc}</td>'
|
||||
f'<td class="table-cell">{_render_snap_val(before_val)}</td>'
|
||||
f'<td class="table-cell">{_render_snap_val(after_val)}</td>'
|
||||
f'<td class="table-cell">{snap_id}</td>'
|
||||
f'<td class="table-cell">{e(user)}</td>'
|
||||
f'</tr>'
|
||||
f'{_snap_expand_row(before_val, after_val, 7)}')
|
||||
select_all = (
|
||||
'<input type="checkbox" '
|
||||
'onchange="document.querySelectorAll(\'[name=selected_uuids]\').forEach(c=>c.checked=this.checked)"/>'
|
||||
|
|
@ -636,25 +641,27 @@ def collect_tokens():
|
|||
done_items = get_dashboard_done()
|
||||
if done_items:
|
||||
hist_rows = ''
|
||||
_hist_onclick = 'onclick="this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
||||
for _uuid, applied_ts in done_items:
|
||||
snap = load_snapshot_for_uuid(_uuid)
|
||||
if applied_ts:
|
||||
dt_str = datetime.fromtimestamp(applied_ts).strftime('%Y-%m-%d %H:%M')
|
||||
else:
|
||||
dt_str = '-'
|
||||
snap_desc = e(snap['description']) if snap else ''
|
||||
before_html = _render_snap_val(snap.get('before') if snap else None)
|
||||
after_html = _render_snap_val(snap.get('after') if snap else None)
|
||||
snap_id = e(_uuid[:8]) if snap else ''
|
||||
snap_user = e(snap['user']) if snap else ''
|
||||
hist_rows += (f'<tr>'
|
||||
snap_desc = e(snap['description']) if snap else ''
|
||||
before_val = snap.get('before') if snap else None
|
||||
after_val = snap.get('after') if snap else None
|
||||
snap_id = e(_uuid[:8]) if snap else ''
|
||||
snap_user = e(snap['user']) if snap else ''
|
||||
hist_rows += (f'<tr style="cursor:pointer" {_hist_onclick}>'
|
||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
||||
f'<td class="table-cell">{snap_desc}</td>'
|
||||
f'<td class="table-cell">{before_html}</td>'
|
||||
f'<td class="table-cell">{after_html}</td>'
|
||||
f'<td class="table-cell">{_render_snap_val(before_val)}</td>'
|
||||
f'<td class="table-cell">{_render_snap_val(after_val)}</td>'
|
||||
f'<td class="table-cell">{snap_id}</td>'
|
||||
f'<td class="table-cell">{snap_user}</td>'
|
||||
f'</tr>')
|
||||
f'</tr>'
|
||||
f'{_snap_expand_row(before_val, after_val, 6)}')
|
||||
history_html = (
|
||||
'<table class="data-table" style="margin-bottom:1rem">'
|
||||
'<thead><tr>'
|
||||
|
|
@ -813,25 +820,42 @@ def e(text):
|
|||
return html_mod.escape(str(text))
|
||||
|
||||
|
||||
def _render_snap_val(val):
|
||||
"""Return an HTML string for a snapshot before/after cell value."""
|
||||
def _snap_text(val):
|
||||
"""Return the plain-text representation of a snapshot before/after value."""
|
||||
if val is None:
|
||||
return ''
|
||||
if isinstance(val, dict) and len(val) == 1:
|
||||
k, v = next(iter(val.items()))
|
||||
text = f'{k}: {v}'
|
||||
elif isinstance(val, (dict, list)):
|
||||
text = json.dumps(val, separators=(',', ':'))
|
||||
else:
|
||||
text = str(val)
|
||||
return f'{k}: {v}'
|
||||
if isinstance(val, (dict, list)):
|
||||
return json.dumps(val, separators=(',', ':'))
|
||||
return str(val)
|
||||
|
||||
|
||||
def _render_snap_val(val):
|
||||
"""Return truncated escaped HTML for a snapshot before/after table cell."""
|
||||
text = _snap_text(val)
|
||||
if not text:
|
||||
return ''
|
||||
trunc = (text[:23] + '…') if len(text) > 24 else text
|
||||
if trunc == text:
|
||||
return e(text)
|
||||
return (f'<details style="display:inline">'
|
||||
f'<summary style="cursor:pointer;list-style:none">{e(trunc)}</summary>'
|
||||
f'<pre style="margin:0.5rem 0;white-space:pre-wrap;font-size:0.85em">'
|
||||
f'{e(json.dumps(val, indent=2) if isinstance(val, (dict, list)) else text)}'
|
||||
f'</pre></details>')
|
||||
return e(trunc)
|
||||
|
||||
|
||||
def _snap_expand_row(before_val, after_val, colspan):
|
||||
"""Return a hidden <tr> that expands with full before/after content."""
|
||||
pre = ('max-height:200px;overflow-y:auto;white-space:pre-wrap;'
|
||||
'font-size:0.85em;background:#fff;border:1px solid #ddd;'
|
||||
'padding:0.5rem;margin:0.25rem 0')
|
||||
def box(label, val):
|
||||
text = _snap_text(val) if val is not None else ''
|
||||
if isinstance(val, (dict, list)):
|
||||
text = json.dumps(val, indent=2)
|
||||
body = e(text) if text else '<em>(none)</em>'
|
||||
return f'<div style="flex:1;min-width:0"><strong>{label}</strong><pre style="{pre}">{body}</pre></div>'
|
||||
inner = f'<div style="display:flex;gap:1rem">{box("Before", before_val)}{box("After", after_val)}</div>'
|
||||
return (f'<tr hidden>'
|
||||
f'<td colspan="{colspan}" style="padding:0.5rem 1rem;background:#f8f8f8">'
|
||||
f'{inner}</td></tr>')
|
||||
|
||||
|
||||
def apply_tokens(text, tokens):
|
||||
|
|
|
|||
|
|
@ -1428,7 +1428,7 @@
|
|||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Errors Only",
|
||||
"label": "Only record errors to log",
|
||||
"name": "log_errors_only",
|
||||
"input_type": "checkbox",
|
||||
"value": "%GENERAL_LOG_ERRORS_ONLY%",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue