# factory.py — JSON content-type renderer # Converts content.json item trees into HTML strings. # Pure type processing: no data loading, no routing, no layout. from flask import session from markupsafe import Markup import json, re, sys, html as html_mod from config_utils import config_hash # Injected by view_page at startup ==================================== # view_page sets this after defining load_datasource so that build_table # can load row data without creating a circular import. load_datasource = None # Constants =========================================================== LEVEL_RANK = {'nothing': 0, 'viewer': 1, 'administrator': 2, 'manager': 3} STANDARD_INPUT_TYPES = {'text', 'password', 'number', 'checkbox', 'select', 'textarea'} # Utilities =========================================================== def e(text): return html_mod.escape(str(text)) def _prefix_to_dotted(n): mask = (0xFFFFFFFF << (32 - n)) & 0xFFFFFFFF return '.'.join(str((mask >> (8 * i)) & 0xFF) for i in (3, 2, 1, 0)) def apply_tokens(text, tokens): """Substitute %TOKEN% placeholders. Values are NOT auto-escaped — callers that use results in HTML attribute or text context must call e() themselves.""" return re.sub(r'%([A-Z_]+)%', lambda m: str(tokens.get(m.group(1), m.group(0))), text) def expand_fields(obj, tokens): """Recursively apply token substitution to a field-definition object. String values that resolve to a JSON array or object are parsed back into Python structures so they serialize correctly into data-fields JSON.""" if isinstance(obj, list): return [expand_fields(item, tokens) for item in obj] if isinstance(obj, dict): out = {} for k, v in obj.items(): if isinstance(v, str): s = apply_tokens(v, tokens) if s != v and s[:1] in ('[', '{'): try: out[k] = json.loads(s) continue except Exception: pass out[k] = s else: out[k] = expand_fields(v, tokens) return out return obj def js_str(value): return json.dumps(str(value)) def get_worker_id(datasource): for prefix in ('config:', 'live:'): if datasource.startswith(prefix): return datasource[len(prefix):] return '' # Access control ====================================================== def client_level(): return LEVEL_RANK.get(session.get('access_level', 'nothing'), 0) def passes(req, level): if not req: return False for suffix, check in (('+', lambda n, l: l >= n), ('-', lambda n, l: l <= n), ('=', lambda n, l: l == n)): if req.endswith(suffix): role = req[:-1].replace('client_is_', '', 1) needed = LEVEL_RANK.get(role) if needed is None: print(f'[factory] WARNING: unknown role "{role}" in client_requirement "{req}"', file=sys.stderr) return False return check(needed, level) print(f'[factory] WARNING: client_requirement "{req}" has no valid suffix (+, -, =)', file=sys.stderr) return False # Snapshot helpers ==================================================== 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())) return f'{k}: {v}' if isinstance(val, (dict, list)): return json.dumps(val, separators=(',', ':')) return str(val) def build_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 return e(trunc) def snap_expand_row(before_val, after_val, colspan): """Return a hidden that expands with full before/after content.""" 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 '(none)' return ( '
' f'{label}' f'
{body}
' ) inner = f'
{box("Before", before_val)}{box("After", after_val)}
' return f'{inner}' # Form helpers ======================================================== def collect_form_specs(items): """Walk form items; return (field_specs, submit_sel) for form script generation.""" fields = [] submit_sel = None for item in items: t = item.get('type', '') if t == 'field': itype = item.get('input_type', 'text') if item.get('validate') or itype == 'checkbox' or itype == 'number': fields.append(item) elif t == 'subnet_row': fields.append(item) elif t == 'button_primary' and item.get('class'): first_cls = item['class'].split()[0] submit_sel = submit_sel or ('.' + first_cls) elif t in ('field_row', 'button_row', 'section', 'form'): sub, sub_btn = collect_form_specs(item.get('items', [])) fields.extend(sub) submit_sel = submit_sel or sub_btn return fields, submit_sel def build_form_script(field_specs, submit_sel): """Generate an inline ' def collect_form_originals(items, tokens): """Walk form items and return {name: value} for all fields (used for original_values).""" result = {} for item in items: t = item.get('type', '') if t == 'field': name = item.get('name', '') input_type = item.get('input_type', 'text') if not name or input_type == 'hidden': continue value = apply_tokens(item.get('value', ''), tokens) if input_type == 'checkbox': result[name] = '1' if value.lower() in ('true', '1', 'yes') else '0' elif input_type == 'select' and not value: try: opts = json.loads(apply_tokens(item.get('options', '[]'), tokens)) value = opts[0]['value'] if opts else '' except Exception: pass result[name] = value else: result[name] = value elif t == 'editable_list': name = item.get('name', '') if not name: continue try: vals = json.loads(apply_tokens(item.get('items', '[]'), tokens)) vals = [str(v) for v in vals] except Exception: vals = [] result[name] = vals elif t == 'subnet_row': result[item.get('subnet_name', 'subnet')] = apply_tokens(item.get('subnet_value', ''), tokens) result[item.get('prefix_name', 'subnet_mask')] = apply_tokens(item.get('prefix_value', '24'), tokens) elif t == 'field_row': result.update(collect_form_originals(item.get('items', []), tokens)) return result # Table-picker component ============================================== def build_table_picker(name, label, value, rows, headers, summary_config, action_btn_html=''): """Generic table-picker dropdown component. rows: list of dicts, each with: key - str: value stored in hidden input and used to identify the row label - str: text shown in the trigger button badge_class - str: CSS class for the badge (optional) badge_label - str: badge text (optional) cells - list[str]: fully-formed ... HTML strings, one per header column summary - dict[str, str]: field→display-value for the button mini-table (optional) extra_data - dict[str, str]: additional data-* attrs on the (optional) headers: list[str]: column header labels for the dropdown table summary_config: list of {field, label, mono?} defining the button mini-table columns action_btn_html: optional extra HTML placed in the picker header (e.g. a Configure button) """ rows_html = '' cur_row = None for row in rows: sel_cls = ' selected' if row['key'] == value else '' if row['key'] == value: cur_row = row attrs = f'data-key="{e(row["key"])}" data-label="{e(row["label"])}"' if row.get('badge_class'): attrs += f' data-badge-class="{e(row["badge_class"])}" data-badge-label="{e(row.get("badge_label", ""))}"' for field, val in (row.get('summary') or {}).items(): attrs += f' data-{e(field)}="{e(str(val))}"' for attr, val in (row.get('extra_data') or {}).items(): attrs += f' data-{e(attr)}="{e(str(val))}"' cells_html = ''.join(row.get('cells', [])) rows_html += f'{cells_html}' thead = ''.join(f'{e(h)}' for h in headers) table_html = ( '
' '' f'{thead}' f'{rows_html}' '
' ) btn_label = f'{e(value) if value else "Select..."}' btn_badge = '' if cur_row and cur_row.get('badge_class'): btn_badge = ( f'' f'{e(cur_row.get("badge_label", ""))}' ) ext_meta = '' if cur_row and summary_config: summary = cur_row.get('summary') or {} if any(summary.get(c['field']) for c in summary_config): hcells = ''.join(f'{e(c["label"])}' for c in summary_config) def _dcell(c): cls = ' class="col-mono"' if c.get('mono') else '' return f'{e(str(summary.get(c["field"]) or "-"))}' dcells = ''.join(_dcell(c) for c in summary_config) ext_meta = ( '' f'{hcells}' f'{dcells}' '
' ) summary_attr = '' if summary_config: summary_json = json.dumps([ {k: v for k, v in c.items() if k in ('field', 'label', 'mono')} for c in summary_config ]) summary_attr = f' data-summary="{e(summary_json)}"' return ( '
' f'' f'
' f'' '
' f'' f'{ext_meta}' f'{action_btn_html}' '
' f'
{table_html}
' '
' '
' ) # Field renderer ====================================================== def build_field(item, tokens): label = e(item.get('label', '')) name = e(item.get('name', '')) input_type = item.get('input_type', 'text') value = apply_tokens(item.get('value', ''), tokens) placeholder = e(apply_tokens(item.get('placeholder', ''), tokens)) hint = e(apply_tokens(item.get('hint', ''), tokens)) hint_html = f'

{hint}

' if hint else '' readonly = ' readonly' if item.get('readonly') else '' if input_type == 'hidden': return f'' if input_type == 'checkbox': checked = 'checked' if value.lower() in ('true', '1', 'yes') else '' cb_label = item.get('checkbox_label') if cb_label: label_html = f'' if label else '' return ( '
' f'{label_html}' '{hint_html}
' ) return ( '
' '{hint_html}
' ) if input_type == 'checkbox_group': try: opts = json.loads(apply_tokens(item.get('options', '[]'), tokens)) selected = json.loads(value) if value else [] except Exception: opts, selected = [], [] boxes = ''.join( '' for o in opts ) return ( f'
' f'
{boxes}
{hint_html}
' ) if input_type == 'select': options = item.get('options', []) if isinstance(options, str): try: options = json.loads(apply_tokens(options, tokens)) except Exception: options = [] current = apply_tokens(item.get('value', ''), tokens) opts_html = ''.join( f'' for o in options ) validate = item.get('validate', '') depends = item.get('depends', []) validate_attr = f' data-validate="{e(validate)}"' if validate else '' depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else '' if validate: return ( f'
' f'
' f'
' f'{hint_html}
' ) return ( f'
' f'' f'{hint_html}
' ) if input_type == 'number': min_attr = f' min="{item["min"]}"' if 'min' in item else '' max_attr = f' max="{item["max"]}"' if 'max' in item else '' validate = item.get('validate', 'positive_int') depends = item.get('depends', []) existing_ids = apply_tokens(item.get('existing_ids', ''), tokens) validate_attr = f' data-validate="{e(validate)}"' depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else '' existing_attr = f' data-existing-ids="{e(existing_ids)}"' if existing_ids else '' dyn_hint_html = '' inp = ( f'' ) if item.get('layout') == 'inline': return ( '
' f'' f'
{inp}{dyn_hint_html}
' f'{hint_html}
' ) return ( f'
' f'
{inp}{dyn_hint_html}
{hint_html}
' ) if input_type == 'textarea': rows = item.get('rows', 4) return ( f'
' f'' f'{hint_html}
' ) if input_type == 'interface_picker': current = apply_tokens(item.get('value', ''), tokens) try: ifaces = json.loads(apply_tokens(item.get('data', '[]'), tokens)) except Exception: ifaces = [] state_map = { 'UP': ('badge-enabled', 'Up'), 'DOWN': ('badge-warning', 'Down'), 'INVALID': ('badge-danger', 'Invalid'), } try: speed_pad = int(tokens.get('NETWORK_INTERFACE_STATS_SPEED_PAD', '0')) except Exception: speed_pad = 0 def _pad(val, width): s = str(val) if val else '-' return ' ' * max(0, width - len(s)) + s picker_rows = [] action_btn_html = '' for ifc in ifaces: iname = ifc.get('name', '') wireless = ifc.get('wireless', False) state = ifc.get('state', 'UNKNOWN') carrier = ifc.get('carrier') raw_speed = ifc.get('speed') raw_mtu = ifc.get('mtu') raw_mac = ifc.get('mac') perm_mac = ifc.get('perm_mac', '') min_mtu = ifc.get('min_mtu') max_mtu = ifc.get('max_mtu') sc, st = state_map.get(state, ('badge-disabled', state.title())) type_txt = 'Wireless' if wireless else 'Wired' carrier_txt = '-' if wireless else ('Yes' if carrier else ('No' if carrier is False else '-')) disp_speed = _pad(raw_speed, speed_pad) disp_mtu = _pad(raw_mtu, 4) picker_rows.append({ 'key': iname, 'label': iname, 'badge_class': sc, 'badge_label': st, 'cells': [ f'{e(iname)}', f'{e(type_txt)}', f'{st}', f'{e(carrier_txt)}', f'{e(disp_speed)}', f'{e(disp_mtu)}', f'{e(raw_mac or "-")}', ], 'summary': { 'speed': disp_speed, 'mtu': disp_mtu, 'mac': raw_mac or '-', }, 'extra_data': { 'perm-mac': perm_mac, 'min-mtu': str(min_mtu) if min_mtu is not None else '', 'max-mtu': str(max_mtu) if max_mtu is not None else '', }, }) if iname == current: action_btn_html = ( '' ) headers = ['Interface', 'Type', 'State', 'Carrier', 'Speed', 'MTU', 'MAC'] summary_config = [ {'field': 'speed', 'label': 'Speed'}, {'field': 'mtu', 'label': 'MTU'}, {'field': 'mac', 'label': 'MAC', 'mono': True}, ] return build_table_picker(name, label, current, picker_rows, headers, summary_config, action_btn_html) validate = item.get('validate', '') depends = item.get('depends', []) validate_attr = f' data-validate="{e(validate)}"' if validate else '' depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else '' if validate: return ( f'
' f'
' f'
' f'{hint_html}
' ) return ( f'
' f'' f'{hint_html}
' ) # Editable list renderer ============================================== def build_editable_list(item, tokens): label = e(item.get('label', '')) name = e(item.get('name', '')) ph = e(apply_tokens(item.get('item_placeholder', ''), tokens)) add_lbl = e(apply_tokens(item.get('add_label', 'Add'), tokens)) hint = e(apply_tokens(item.get('hint', ''), tokens)) hint_html = f'

{hint}

' if hint else '' validate = e(item.get('validate', '')) try: items_list = json.loads(apply_tokens(item.get('items', '[]'), tokens)) except Exception: items_list = [] rows = ''.join( '
' f'' '' '
' for v in items_list ) validate_attr = f' data-validate="{validate}"' if validate else '' return ( f'
' f'
' f'{rows}' f'' f'
{hint_html}
' ) # Table worker script ================================================= def build_table_worker_script(item, expanded_ra_fields): """Emit a \n' ) return f'\n' # Table cell renderer ================================================= def build_table_cell(value, render_fn, col_class='', field='', row_idx=None, toggle_action=None, toggle_allowed=True, render_options=None): parts = [] if col_class: parts.append(f'class="{e(col_class)}"') if field: parts.append(f'data-field="{e(field)}"') td_open = f'' if parts else '' if not render_fn: return f'{td_open}{e(value)}' if render_fn == 'badge_enabled_disabled': if str(value).lower() in ('true', '1', 'yes', 'enabled'): inner = 'Enabled' else: inner = 'Disabled' return f'{td_open}{inner}' if render_fn == 'badge_yes_no': opts = render_options or {} if str(value).lower() in ('true', '1', 'yes', 'enabled'): tip = f' data-tooltip="{e(opts["title_true"])}"' if opts.get('title_true') else '' inner = f'Yes' else: tip = f' data-tooltip="{e(opts["title_false"])}"' if opts.get('title_false') else '' inner = f'No' return f'{td_open}{inner}' if render_fn == 'badge_recording_on_off': if str(value).lower() in ('true', '1', 'yes'): inner = 'Recording On' else: inner = 'Recording Off' return f'{td_open}{inner}' if render_fn == 'badge_toggle': if str(value).lower() in ('true', '1', 'yes', 'enabled'): label = 'Enabled'; badge_cls = 'badge-enabled' else: label = 'Disabled'; badge_cls = 'badge-disabled' if toggle_action and row_idx is not None and toggle_allowed: inner = ( f'
' f'' '
' ) else: inner = f'{label}' return f'{td_open}{inner}' if render_fn == 'badge_active_inactive': badges = {'active': 'badge-enabled', 'pending': 'badge-warning'} cls = badges.get(value.lower(), 'badge-disabled') return f'{td_open}{e(value.title())}' if render_fn == 'raw_html': return f'{td_open}{value}' if render_fn == 'tag_list': try: items = json.loads(value) if value.startswith('[') else [s.strip() for s in value.split(',')] except Exception: items = [value] def _tag(t): if isinstance(t, dict): s, tooltip = str(t.get('n', '')).strip(), str(t.get('d', t.get('n', ''))).strip() short = str(t['short']).strip() if 'short' in t else s.split('-')[0] mini = str(t['mini']).strip() if 'mini' in t else (s[0] if s else '') else: s = tooltip = str(t).strip() short = s.split('-')[0] mini = s[0] if s else '' if not s: return '' return ( f'' f'{e(s)}' f'{e(short)}' f'{e(mini)}' '' ) tags = ''.join(_tag(t) for t in items) return f'{td_open}
{tags}
' if render_fn == 'interface_status': v = value.upper() if v == 'INVALID': inner = 'Invalid' elif v == 'UP': inner = 'Up' elif v == 'DOWN': inner = 'Down' else: inner = f'{e(value.title())}' return f'{td_open}{inner}' return f'{td_open}{e(value)}' # Table renderer ====================================================== def build_table(item, tokens, inherited_req=None): level = client_level() columns = item.get('columns', []) rows = load_datasource(item.get('datasource', '')) empty = e(item.get('empty_message', 'No data.')) row_actions = item.get('row_actions', []) hash_val = config_hash() toolbar_html = '' toolbar = item.get('toolbar') if toolbar: req = toolbar.get('client_requirement', inherited_req) if passes(req, level): t_inner = build_items(toolbar.get('items', []), tokens, req) toolbar_html = f'
{t_inner}
' thead = ''.join( f'{e(c.get("label",""))}' if c.get("class") else f'{e(c.get("label",""))}' for c in columns ) if row_actions: thead += '' expanded_ra_fields = { i: expand_fields(ra.get('fields', []), tokens) for i, ra in enumerate(row_actions) if ra.get('method', 'post').lower() == 'inline_edit' } if not rows: colspan = len(columns) + (1 if row_actions else 0) tbody = f'{empty}' else: tbody = '' for idx, row in enumerate(rows): cells = '' for col in columns: val = row for part in col.get('field', '').split('.'): val = val.get(part, '') if isinstance(val, dict) else '' col_req = col.get('client_requirement', inherited_req) toggle_allowed = passes(col_req, level) if col_req else True cells += build_table_cell( str(val) if val != '' else '-', col.get('render', ''), col.get('class', ''), field=col.get('field', ''), row_idx=idx, toggle_action=col.get('toggle_action'), toggle_allowed=toggle_allowed, render_options=col.get('render_options', {}), ) if row_actions: btns = '' for ra_i, ra in enumerate(row_actions): req = ra.get('client_requirement', inherited_req) if not passes(req, level): continue text = e(ra.get('text', '')) cls = e(ra.get('class', 'btn-ghost btn-sm')) action = e(apply_tokens(ra.get('action', '#'), tokens)) method = ra.get('method', 'post').lower() if method == 'post': disable_if = ra.get('disable_if') if disable_if and row.get(disable_if.get('field')) == disable_if.get('value'): btns += f'' continue btns += ( f'
' f'' f'' f'
' ) elif method == 'js_edit': target = e(ra.get('target', 'edit-form')) row_json = e(json.dumps(row)) btns += ( f'' ) elif method == 'inline_edit': expanded = expanded_ra_fields.get(ra_i, []) fields_json = e(json.dumps(expanded)) row_json = e(json.dumps(row)) worker_id = get_worker_id(item.get('datasource', '')) has_nonstandard = any( f.get('input_type', 'text') not in STANDARD_INPUT_TYPES for f in expanded ) worker_attr = f' data-worker-id="{e(worker_id)}"' if has_nonstandard and worker_id else '' btns += ( f'' ) else: btns += f'{text}' cells += f'{btns}' tbody += f'{cells}' worker_script = build_table_worker_script(item, expanded_ra_fields) return ( f'{toolbar_html}' '
' '' f'{thead}' f'{tbody}' f'
{worker_script}' ) # Main dispatcher ===================================================== def build_items(items, tokens, inherited_req=None): level = client_level() parts = [] for item in items: req = item.get('client_requirement', inherited_req) if not passes(req, level): continue parts.append(build_item(item, tokens, req)) return ''.join(parts) def build_item(item, tokens, inherited_req=None): t = item.get('type', '') req = item.get('client_requirement', inherited_req) if t == 'h1': return f'

{e(apply_tokens(item.get("text", ""), tokens))}

' if t == 'hr': return '
' if t == 'p': text = e(apply_tokens(item.get('text', ''), tokens)) link = item.get('link') if link: href = e(apply_tokens(link.get('action', '#'), tokens)) ltext = e(apply_tokens(link.get('text', ''), tokens)) return f'

{text} {ltext}

' return f'

{text}

' if t == 'spacer': return '
' if t in ('button_primary', 'button_secondary', 'button_danger', 'button_ghost'): cls_map = { 'button_primary': 'btn-primary', 'button_secondary': 'btn-secondary', 'button_danger': 'btn-danger', 'button_ghost': 'btn-ghost', } cls = cls_map[t] extra = item.get('class', '') if extra: cls = f'{cls} {extra}' text = e(apply_tokens(item.get('text', ''), tokens)) action_raw = item.get('action', '') action = e(apply_tokens(action_raw, tokens)) disabled_val = apply_tokens(str(item.get('disabled', '')), tokens) disabled = ' disabled' if disabled_val and disabled_val not in ('false', '0') else '' formaction = item.get('formaction', '') if formaction: formaction = e(apply_tokens(formaction, tokens)) return f'' if item.get('method', '').lower() == 'post': return ( f'
' f'
' ) if action_raw: return f'{text}' return f'' if t == 'button_cancel': text = e(apply_tokens(item.get('text', 'Cancel'), tokens)) extra_cls = (' ' + item['class']) if item.get('class') else '' return f'' if t == 'header_page_title': return f'' if t in ('section', 'auth_wrapper'): tag = 'div' cls = 'auth-wrapper' if t == 'auth_wrapper' else 'section' return f'<{tag} class="{cls}">{build_items(item.get("items", []), tokens, req)}' if t == 'auth_card': return f'
{build_items(item.get("items", []), tokens, req)}
' if t == 'stat_card_grid': return f'
{build_items(item.get("items", []), tokens, req)}
' if t == 'stat_card': label = e(apply_tokens(item.get('label', ''), tokens)) raw_value = apply_tokens(item.get('value', ''), tokens) value = e(raw_value) sub = e(apply_tokens(item.get('sub', ''), tokens)) variant = item.get('variant', '') cls = f'stat-card{(" stat-card-" + variant) if variant else ""}' edit_action = item.get('edit_action', '') edit_field = item.get('edit_field', '') edit_input_type = item.get('edit_input_type', 'text') edit_suffix = item.get('edit_suffix', '') edit_min = item.get('edit_min', '') edit_raw = apply_tokens(item.get('edit_value', item.get('value', '')), tokens) reveal_card_id = item.get('reveal_card_id', '') if reveal_card_id: return ( f'
' f'
{label}
' '
' f'{value}' '' '
' f'
{sub}
' '
' ) if edit_action and edit_field: min_attr = f' min="{e(edit_min)}"' if edit_min else '' suffix_html = f'{e(edit_suffix)}' if edit_suffix else '' input_wrap = ( '
' f'' f'{suffix_html}
' ) return ( f'
' f'
{label}
' '
' f'{value}' '' '
' f'' f'
{sub}
' '
' ) return ( f'
' f'
{label}
' f'
{value}
' f'
{sub}
' '
' ) if t == 'card': label = item.get('label', '') id_attr = f' id="{e(item["id"])}"' if item.get('id') else '' cls_hidden = ' hidden' if item.get('hidden') else '' header = f'

{e(label)}

' if label else '' body = build_items(item.get('items', []), tokens, req) return f'
{header}
{body}
' if t == 'field_status': label = e(item.get('label', '')) raw = apply_tokens(item.get('value', ''), tokens).upper() badge_map = { 'UP': ('badge-enabled', 'Up'), 'DOWN': ('badge-warning', 'Down'), 'INVALID': ('badge-danger', 'Invalid'), } badge_cls, badge_text = badge_map.get(raw, ('badge-disabled', raw.title() or 'Unknown')) return ( '
' f'' f'
{badge_text}
' '
' ) if t == 'info_bar': variant = item.get('variant', 'info') text = e(apply_tokens(item.get('text', ''), tokens)) return f'
{text}
' if t == 'pre_block': text = e(apply_tokens(item.get('text', ''), tokens)) extra = ' data-scroll-bottom' if item.get('scroll_to_bottom') else '' return f'
{text}
' if t == 'credential_fields': psel = e(item.get('provider_select', 'provider')) return ( f'
' '' '' '
' ) if t == 'grid': rows_html = '' for row in item.get('rows', []): cells = ''.join(build_item(c, tokens, req) for c in row.get('cells', [])) rows_html += f'
{cells}
' return f'
{rows_html}
' if t == 'grid_label': return f'
{e(apply_tokens(item.get("text", ""), tokens))}
' if t == 'grid_value': return f'
{e(apply_tokens(item.get("text", ""), tokens))}
' if t == 'form': action = e(apply_tokens(item.get('action', ''), tokens)) method = e(item.get('method', 'post')) inner = build_items(item.get('items', []), tokens, req) hash_field = f'' originals = collect_form_originals(item.get('items', []), tokens) orig_field = ( f'' if originals else '' ) field_specs, submit_sel = collect_form_specs(item.get('items', [])) script = build_form_script(field_specs, submit_sel) if (field_specs and submit_sel) else '' return f'
{hash_field}{orig_field}{inner}
{script}' if t == 'hidden': name = e(item.get('name', '')) value = e(apply_tokens(item.get('value', ''), tokens)) return f'' if t == 'record_editor': label = e(item.get('label', '')) name = e(item.get('name', '')) empty = e(item.get('empty_message', 'No records added.')) fields = item.get('fields', []) col_count = len(fields) + 1 ths = ''.join(f'{e(f.get("label",""))}' for f in fields) + '' form_rows = '' for f in fields: f_label = e(f.get('label', '')) f_name = e(f.get('name', '')) f_placeholder = e(f.get('placeholder', '')) f_required = 'true' if f.get('required') else 'false' f_validate = f.get('validate', '') f_valtype = f.get('valtype', '') f_attrs = f.get('attrs', {}) attr_str = f' data-field="{f_name}" data-required="{f_required}"' if f_validate: attr_str += f' data-validate="{e(f_validate)}"' if f_valtype: attr_str += f' data-valtype="{e(f_valtype)}"' for ak, av in f_attrs.items(): attr_str += f' {e(ak)}="{e(str(av))}"' inp = f'' if f_validate or f_valtype: field_inner = ( '
' + inp + '' '
' ) else: field_inner = inp form_rows += ( f'' f'{f_label}:' f'{field_inner}' f'' ) return ( f'
' f'
' f'
' f'' f'' f'{ths}' f'' f'' f'' f'' f'' f'
{empty}
' f'
' f'
' f'{form_rows}
' f'
' f'' f'' f'
' f'
' f'
' f'' f'
' ) if t == 'readonly_select': label = e(item.get('label', 'Gateway')) name = e(item.get('name', 'gateway')) return ( f'
' f'' f'' f'
' ) if t == 'overridable_textarea': label = e(item.get('label', '')) name = e(item.get('name', '')) override_name = e(item.get('override_name', name + '_override')) validate = e(item.get('validate', '')) validate_attr = f' data-validate-lines="{validate}"' if validate else '' hint_html = '' if validate else '' wrap_open = '
' if validate else '' wrap_close = '
' if validate else '' return ( f'
' f'' f'{wrap_open}' f'' f'{hint_html}' f'{wrap_close}' f'
' ) if t == 'field': return build_field(item, tokens) if t == 'field_row': inner = build_items(item.get('items', []), tokens, req) cols = item.get('cols', 2) return f'
{inner}
' if t == 'subnet_row': subnet_label = e(item.get('label', 'Subnet')) subnet_name = e(item.get('subnet_name', 'subnet')) prefix_name = e(item.get('prefix_name', 'subnet_mask')) subnet_val = apply_tokens(item.get('subnet_value', ''), tokens) prefix_raw = apply_tokens(item.get('prefix_value', '24'), tokens) subnet_ph = e(apply_tokens(item.get('subnet_placeholder', ''), tokens)) try: pf = max(1, min(30, int(prefix_raw))) except (ValueError, TypeError): pf = 24 dotted = _prefix_to_dotted(pf) return ( '
' f'' '
' '
' f'' '/' f'' '
' f'{e(dotted)}' '' '
' '
' ) if t == 'editable_list': return build_editable_list(item, tokens) if t == 'select': name = e(item.get('name', '')) options = apply_tokens(item.get('options', ''), tokens) filter_col = item.get('filter_col', '') extra = f' data-filter-col="{e(filter_col)}"' if filter_col else '' return f'' if t == 'spacer': return '' if t == 'button_row': justify = item.get('justify', '') style_attr = f' style="justify-content:{e(justify)}"' if justify else '' inner = build_items(item.get('items', []), tokens, req) return f'
{inner}
' if t == 'table': return build_table(item, tokens, req) if t == 'raw_html': return Markup(apply_tokens(item.get('html', ''), tokens)) return ''