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