Development

This commit is contained in:
Matthew Grotke 2026-06-07 15:11:40 -04:00
parent 27f2356cd1
commit 19f0bfa79c
4 changed files with 115 additions and 21 deletions

View file

@ -25,10 +25,20 @@ DASHB_INTERVAL_SECS = 60
QUEUE_MAX_LINES = 50
_config_cache = None
_config_mtime = None
def load_config():
global _config_cache, _config_mtime
try:
mtime = os.path.getmtime(CONFIG_FILE)
if _config_cache is not None and mtime == _config_mtime:
return copy.deepcopy(_config_cache)
with open(CONFIG_FILE) as f:
return json.load(f)
data = json.load(f)
_config_cache = data
_config_mtime = mtime
return copy.deepcopy(data)
except Exception:
return {}

View file

@ -35,19 +35,38 @@ def options_save():
return redirect(f'/{_PAGE}')
@bp.route('/action/captiveportal/splash_save', methods=['POST'])
@bp.route('/action/captiveportal/portal_save', methods=['POST'])
@auth.require_level('administrator')
def splash_save():
cfg = config_utils.load_config()
before = copy.deepcopy(cfg.get('captive_portal', {}))
def portal_save():
cfg = config_utils.load_config()
vlan_name = sanitize.name(request.form.get('vlan_name', ''))
splash_text = sanitize.description(request.form.get('splash_text', ''))
terms = [t.strip() for t in request.form.getlist('terms') if t.strip()]
vlan = next((v for v in cfg.get('vlans', []) if v['name'] == vlan_name), None)
if not vlan or vlan.get('restricted_vlan') != 'c':
flash('Captive portal VLAN not found.', 'error')
return redirect(f'/{_PAGE}')
after = {**before, 'splash_text': splash_text, 'terms': terms}
cfg.setdefault('captive_portal', {}).update(after)
before = {
'portal_splash_title': vlan.get('portal_splash_title', ''),
'portal_splash_text': vlan.get('portal_splash_text', ''),
'portal_terms': vlan.get('portal_terms', []),
}
splash_title = sanitize.description(request.form.get('portal_splash_title', ''))
splash_text = sanitize.description(request.form.get('portal_splash_text', ''))
terms = [t.strip() for t in request.form.getlist('portal_terms') if t.strip()]
vlan['portal_splash_title'] = splash_title
vlan['portal_splash_text'] = splash_text
vlan['portal_terms'] = terms
after = {
'portal_splash_title': splash_title,
'portal_splash_text': splash_text,
'portal_terms': terms,
}
changes = config_utils.diff_fields(before, after)
flash(config_utils.record_group(
cfg, 'captive_portal', 'setting', 'captive_portal', changes, 'core apply'
cfg, 'vlans', 'portal', vlan_name, changes, 'core apply'
), 'success')
return redirect(f'/{_PAGE}')

View file

@ -64,32 +64,81 @@
}
]
},
{
"type": "table",
"datasource": "captive_portals",
"empty_message": "No captive portal VLANs configured. Set Restricted VLAN = Captive Portal on the Network Layout page.",
"columns": [
{
"label": "VLAN",
"field": "vlan_name",
"class": "col-mono"
},
{
"label": "Title",
"field": "portal_splash_title"
},
{
"label": "Splash Text",
"field": "portal_splash_text"
},
{
"label": "Terms",
"field": "portal_terms_display",
"class": "col-narrow"
}
],
"row_actions": [
{
"client_requirement": "client_is_administrator+",
"method": "js_edit",
"target": "portal-edit-form",
"text": "Edit",
"class": "btn-ghost btn-sm"
}
]
},
{
"type": "card",
"label": "Splash Screen",
"id": "portal-edit-form",
"label": "Edit Portal",
"client_requirement": "client_is_administrator+",
"items": [
{
"type": "form",
"action": "/action/captiveportal/splash_save",
"action": "/action/captiveportal/portal_save",
"method": "post",
"items": [
{
"type": "hidden",
"name": "vlan_name",
"value": ""
},
{
"type": "field",
"label": "Welcome Text",
"name": "splash_text",
"label": "Title",
"name": "portal_splash_title",
"input_type": "text",
"value": "%CAPTIVE_SPLASH_TEXT%",
"hint": "Welcome message shown at the top of the login page."
"hint": "Heading shown at the top of the login page."
},
{
"type": "field",
"label": "Splash Text",
"name": "portal_splash_text",
"input_type": "text",
"hint": "Welcome message shown below the title."
},
{
"type": "hr"
},
{
"type": "editable_list",
"label": "Terms",
"name": "terms",
"items": "%CAPTIVE_TERMS%",
"name": "portal_terms",
"items": "[]",
"add_label": "Add Term",
"item_placeholder": "e.g. I agree to the acceptable use policy.",
"hint": "Each term renders as a required checkbox the user must tick before submitting credentials."
"hint": "Each term renders as a required checkbox the user must tick before submitting credentials. Leave empty for no terms."
},
{
"type": "button_row",

View file

@ -20,7 +20,23 @@ def collect_tokens(cfg):
tokens['CAPTIVE_HTTP_PORT'] = str(cp.get('http_port', 8081))
tokens['CAPTIVE_HTTPS_DOMAIN'] = cp.get('https_domain', '')
tokens['CAPTIVE_SPLASH_TEXT'] = cp.get('splash_text', '')
tokens['CAPTIVE_TERMS'] = json.dumps(cp.get('terms', []))
display_rows = []
for vlan in captive_vlans:
terms = vlan.get('portal_terms', [])
n = len(terms)
display_rows.append({
'vlan_name': vlan['name'],
'portal_splash_title': vlan.get('portal_splash_title', ''),
'portal_splash_text': vlan.get('portal_splash_text', ''),
'portal_terms': terms,
'portal_terms_display': f'{n} term{"s" if n != 1 else ""}' if n else '--',
})
content = factory.load_json(f'{factory.PAGES_DIR}/captiveportal/content.json')
for table_item in factory.iter_table_items(content.get('items', [])):
ds = table_item.get('datasource', '')
data = display_rows if ds == 'captive_portals' else []
tokens[factory.table_token_key(ds)] = factory.build_table(table_item, tokens, data)
return tokens