2026-05-28 22:50:00 -04:00
# 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 ' }
2026-05-31 00:22:39 -04:00
_VALIDATION_FLAGS = {
' VALIDATION_IPV4_FORMAT ' : 1 ,
' VALIDATION_IPV6_FORMAT ' : 2 ,
' VALIDATION_SUBNET ' : 4 ,
' VALIDATION_ADDRESS ' : 8 ,
' VALIDATION_MAC ' : 16 ,
' VALIDATION_URL ' : 32 ,
' VALIDATION_PORT ' : 64 ,
' VALIDATION_DASH_NAME ' : 128 ,
' VALIDATION_NETWORK_NAME ' : 256 ,
' VALIDATION_DOMAIN_NAME ' : 512 ,
' VALIDATION_TIME24H ' : 1024 ,
' VALIDATION_RANGE_INT ' : 2048 ,
' VALIDATION_ENDPOINT ' : 4096 ,
' VALIDATION_IPV4_CIDR ' : 8192 ,
}
_COMPAT_VALIDATION = {
' ipv4 ' : ' VALIDATION_IPV4_FORMAT ' ,
' ipv6 ' : ' VALIDATION_IPV6_FORMAT ' ,
' ip ' : ' VALIDATION_IPV4_FORMAT|VALIDATION_IPV6_FORMAT ' ,
' ipv4cidr ' : ' VALIDATION_IPV4_CIDR ' ,
' mac ' : ' VALIDATION_MAC ' ,
' url ' : ' VALIDATION_URL ' ,
' port ' : ' VALIDATION_PORT ' ,
' dashname ' : ' VALIDATION_DASH_NAME ' ,
' networkname ' : ' VALIDATION_NETWORK_NAME ' ,
' domainname ' : ' VALIDATION_DOMAIN_NAME ' ,
' time_24h ' : ' VALIDATION_TIME24H ' ,
' vlan_id ' : ' VALIDATION_RANGE_INT ' ,
' positive_int ' : ' VALIDATION_RANGE_INT ' ,
' endpoint ' : ' VALIDATION_ENDPOINT ' ,
' ip_in_subnet ' : ' VALIDATION_ADDRESS ' ,
' address ' : ' VALIDATION_ADDRESS ' ,
' subnet ' : ' VALIDATION_SUBNET ' ,
}
2026-05-28 22:50:00 -04:00
# 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 ) )
2026-05-31 00:22:39 -04:00
def parse_validation ( s ) :
if not s :
return 0
resolved = _COMPAT_VALIDATION . get ( s , s )
result = 0
for token in resolved . split ( ' | ' ) :
token = token . strip ( )
val = _VALIDATION_FLAGS . get ( token )
if val is None :
print ( f ' [factory] WARNING: unknown validation token " { token } " in " { s } " ' , file = sys . stderr )
continue
result | = val
return result
def _encode_field_validations ( fields ) :
out = [ ]
for f in fields :
f2 = dict ( f )
raw = f2 . get ( ' validate ' , ' ' )
if not raw and f2 . get ( ' input_type ' ) == ' number ' :
raw = ' VALIDATION_RANGE_INT '
if raw and isinstance ( raw , str ) :
f2 [ ' validate ' ] = parse_validation ( raw )
out . append ( f2 )
return out
def build_big_validate ( ) :
_JS_NAMES = {
' VALIDATION_IPV4_FORMAT ' : ' F_IPV4 ' ,
' VALIDATION_IPV6_FORMAT ' : ' F_IPV6 ' ,
' VALIDATION_SUBNET ' : ' F_SUBNET ' ,
' VALIDATION_ADDRESS ' : ' F_ADDR ' ,
' VALIDATION_MAC ' : ' F_MAC ' ,
' VALIDATION_URL ' : ' F_URL ' ,
' VALIDATION_PORT ' : ' F_PORT ' ,
' VALIDATION_DASH_NAME ' : ' F_DASH ' ,
' VALIDATION_NETWORK_NAME ' : ' F_NET ' ,
' VALIDATION_DOMAIN_NAME ' : ' F_DOMAIN ' ,
' VALIDATION_TIME24H ' : ' F_T24H ' ,
' VALIDATION_RANGE_INT ' : ' F_RNGINT ' ,
' VALIDATION_ENDPOINT ' : ' F_ENDPT ' ,
' VALIDATION_IPV4_CIDR ' : ' F_IPV4C ' ,
}
decls = ' ' . join (
f ' var { _JS_NAMES [ k ] } = { _VALIDATION_FLAGS [ k ] } ; '
for k in _VALIDATION_FLAGS
)
body = r """
function _ok ( ) { return { ok : true , msg : ' ' , partial : false } ; }
function _par ( m ) { return { ok : false , msg : m | | ' ' , partial : true } ; }
function _err ( m ) { return { ok : false , msg : m | | ' Invalid ' , partial : false } ; }
function _ipv4 ( s ) {
if ( ! s ) return ' empty ' ;
if ( / [ ^ 0 - 9. ] / . test ( s ) ) return ' badchar ' ;
if ( / \. \. / . test ( s ) | | s [ 0 ] == = ' . ' ) return ' badstruct ' ;
var p = s . split ( ' . ' ) ;
if ( p . length > 4 ) return ' badstruct ' ;
for ( var i = 0 ; i < p . length ; i + + ) { if ( ! p [ i ] ) continue ; var n = parseInt ( p [ i ] , 10 ) ; if ( isNaN ( n ) | | n > 255 | | String ( n ) != = p [ i ] ) return ' badrange ' ; }
return ( p . length == = 4 & & p . every ( function ( x ) { return x != = ' ' ; } ) ) ? ' ok ' : ' partial ' ;
}
function _ipv6 ( s ) {
if ( ! s ) return ' empty ' ;
if ( / [ ^ 0 - 9 a - fA - F : ] / . test ( s ) ) return ' badchar ' ;
if ( / : : : / . test ( s ) ) return ' badstruct ' ;
if ( ( s . match ( / : : / g ) | | [ ] ) . length > 1 ) return ' badstruct ' ;
var parts = s . split ( / : : ? / ) ;
for ( var i = 0 ; i < parts . length ; i + + ) { if ( parts [ i ] . length > 4 ) return ' badstruct ' ; }
var c = ( s . match ( / : / g ) | | [ ] ) . length , d = s . indexOf ( ' :: ' ) != = - 1 ;
if ( d & & c > 7 ) return ' badstruct ' ;
return ( c == = 7 & & ! d ) | | d ? ' ok ' : ' partial ' ;
}
function _checkFlag ( s , flag ) {
if ( flag == = F_IPV4 ) { var r = _ipv4 ( s ) ; if ( r == = ' ok ' ) return _ok ( ) ; if ( r == = ' partial ' | | r == = ' empty ' ) return _par ( ' ' ) ; if ( r == = ' badchar ' ) return _err ( ' Invalid character ' ) ; if ( r == = ' badrange ' ) return _err ( ' Octet out of range ' ) ; return _err ( ' Invalid format ' ) ; }
if ( flag == = F_IPV6 ) { var r = _ipv6 ( s ) ; if ( r == = ' ok ' ) return _ok ( ) ; if ( r == = ' partial ' | | r == = ' empty ' ) return _par ( ' ' ) ; if ( r == = ' badchar ' ) return _err ( ' Invalid character ' ) ; return _err ( ' Invalid format ' ) ; }
if ( flag == = F_MAC ) { if ( ! s ) return _par ( ' ' ) ; if ( / [ ^ 0 - 9 a - fA - F : ] / . test ( s ) ) return _err ( ' Invalid character ' ) ; if ( / : : / . test ( s ) ) return _err ( ' Invalid format ' ) ; var g = s . split ( ' : ' ) ; if ( g . length > 6 ) return _err ( ' Too many groups ' ) ; for ( var i = 0 ; i < g . length ; i + + ) { if ( g [ i ] . length > 2 ) return _err ( ' Each group must be exactly 2 hex characters ' ) ; } return ( g . length == = 6 & & g . every ( function ( x ) { return x . length == = 2 ; } ) ) ? _ok ( ) : _par ( ' ' ) ; }
if ( flag == = F_URL ) { if ( ! s ) return _par ( ' ' ) ; if ( / [ ^ A - Za - z0 - 9 \- . _ ~ : / ? #\[\]@!$&'()*+,;=%]/.test(s))return _err('Invalid character');var sl=s.toLowerCase();if('https://'.startsWith(sl)||'http://'.startsWith(sl))return _par('');var sep=sl.indexOf('://');if(sep===-1)return _err('Invalid URL format');var scheme=sl.slice(0,sep);if(scheme!=='http'&&scheme!=='https')return _err('Invalid URL format');var after=s.slice(sep+3);if(!after)return _par('');var he=after.search(/[/:?#]/),host=he===-1?after:after.slice(0,he),rest=he===-1?'':after.slice(he);if(!host)return _par('');if(/\.\./.test(host)||host[0]==='.'||host[host.length-1]==='.')return _err('Invalid URL format');var lb=host.split('.');for(var i=0;i<lb.length;i++){if(!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(lb[i]))return _err('Invalid URL format');}if(rest[0]===':'){var pm=rest.slice(1).match(/^\d+/);if(!pm)return _par('');if(parseInt(pm[0])<1||parseInt(pm[0])>65535)return _err('Invalid URL format');}return _ok();}
if ( flag == = F_PORT ) { if ( ! s ) return _par ( ' ' ) ; if ( / [ ^ 0 - 9 ] / . test ( s ) ) return _err ( ' Digits only ' ) ; var n = parseInt ( s , 10 ) ; return ( n > = 1 & & n < = 65535 ) ? _ok ( ) : _err ( ' Must be between 1 and 65535 ' ) ; }
if ( flag == = F_DASH ) { if ( ! s ) return _par ( ' ' ) ; if ( / [ ^ a - z0 - 9 - ] / . test ( s ) ) return _err ( ' Lowercase letters, digits and hyphens only ' ) ; if ( s [ 0 ] == = ' - ' | | / - - / . test ( s ) ) return _err ( ' No leading, trailing or consecutive hyphens ' ) ; if ( s [ s . length - 1 ] == = ' - ' ) return _par ( ' ' ) ; return _ok ( ) ; }
if ( flag == = F_NET ) { if ( ! s ) return _par ( ' ' ) ; if ( / [ ^ a - zA - Z0 - 9 _ - ] / . test ( s ) ) return _err ( ' Letters, digits, hyphens and underscores only ' ) ; if ( s [ 0 ] == = ' - ' | | s [ 0 ] == = ' _ ' ) return _err ( ' No leading, trailing or consecutive special characters ' ) ; if ( / [ - _ ] { 2 , } / . test ( s ) ) return _err ( ' No leading, trailing or consecutive special characters ' ) ; if ( s [ s . length - 1 ] == = ' - ' | | s [ s . length - 1 ] == = ' _ ' ) return _par ( ' ' ) ; return _ok ( ) ; }
if ( flag == = F_DOMAIN ) { if ( ! s ) return _par ( ' ' ) ; if ( / [ ^ a - zA - Z0 - 9. - ] / . test ( s ) ) return _err ( ' Letters, digits, hyphens and dots only ' ) ; if ( s [ 0 ] == = ' . ' ) return _err ( ' Invalid domain format ' ) ; if ( / \. \. / . test ( s ) ) return _err ( ' Invalid domain format ' ) ; if ( s [ s . length - 1 ] == = ' . ' ) return _par ( ' ' ) ; var lb = s . split ( ' . ' ) ; for ( var i = 0 ; i < lb . length ; i + + ) { var l = lb [ i ] ; if ( l [ 0 ] == = ' - ' | | l [ l . length - 1 ] == = ' - ' ) return _err ( ' Invalid domain format ' ) ; } return _ok ( ) ; }
if ( flag == = F_T24H ) { if ( ! s ) return _par ( ' ' ) ; if ( / [ ^ 0 - 9 : ] / . test ( s ) ) return _err ( ' Digits and colon only ' ) ; if ( s . length < 5 ) return _par ( ' ' ) ; return / ^ ( [ 01 ] \d | 2 [ 0 - 3 ] ) : [ 0 - 5 ] \d $ / . test ( s ) ? _ok ( ) : _err ( ' Must be HH:MM in 24-hour format (e.g. 02:30) ' ) ; }
if ( flag == = F_RNGINT ) { if ( s == = ' ' | | s == = null | | s == = undefined ) return _par ( ' ' ) ; if ( / [ ^ 0 - 9 ] / . test ( s ) ) return _err ( ' Digits only ' ) ; var n = parseInt ( s , 10 ) ; var mn = ( arg1 != = ' ' & & arg1 != null ) ? parseInt ( arg1 , 10 ) : 0 ; var mx = ( arg2 != = ' ' & & arg2 != null ) ? parseInt ( arg2 , 10 ) : null ; if ( n < mn | | ( mx != = null & & n > mx ) ) { if ( mn != null & & mx != = null ) return _err ( ' Must be between ' + mn + ' and ' + mx ) ; return mn != null ? _err ( ' Must be >= ' + mn ) : _err ( ' Must be <= ' + mx ) ; } return _ok ( ) ; }
if ( flag == = F_ENDPT ) { if ( ! s ) return _par ( ' ' ) ; if ( / ^ [ 0 - 9. ] + $ / . test ( s ) ) { var r = _ipv4 ( s ) ; return r == = ' ok ' ? _ok ( ) : ( r == = ' partial ' | | r == = ' empty ' ) ? _par ( ' ' ) : _err ( ' Invalid character ' ) ; } if ( s . indexOf ( ' : ' ) != = - 1 ) { var cc = ( s . match ( / : / g ) | | [ ] ) . length ; if ( cc > 1 ) { if ( / : : : / . test ( s ) | | ( s . match ( / : : / g ) | | [ ] ) . length > 1 ) return _err ( ' Invalid hostname or IP ' ) ; if ( / [ ^ 0 - 9 a - fA - F : . ] / . test ( s ) ) return _err ( ' Invalid character ' ) ; var col = s . replace ( / [ ^ : ] / g , ' ' ) . length ; return ( s . indexOf ( ' :: ' ) != = - 1 | | col == = 7 ) ? _ok ( ) : _par ( ' ' ) ; } return _checkFlag ( s . slice ( 0 , s . lastIndexOf ( ' : ' ) ) , F_DOMAIN ) ; } return _checkFlag ( s , F_DOMAIN ) ; }
if ( flag == = F_IPV4C ) { if ( ! s ) return _par ( ' ' ) ; var slash = s . indexOf ( ' / ' ) ; if ( slash == = - 1 ) { var r = _ipv4 ( s ) ; return ( r == = ' ok ' | | r == = ' partial ' | | r == = ' empty ' ) ? _par ( ' ' ) : ( r == = ' badchar ' ? _err ( ' Invalid character ' ) : r == = ' badrange ' ? _err ( ' Octet out of range ' ) : _err ( ' Invalid format ' ) ) ; } var r = _ipv4 ( s . slice ( 0 , slash ) ) ; if ( r != = ' ok ' ) return r == = ' badchar ' ? _err ( ' Invalid character ' ) : r == = ' badrange ' ? _err ( ' Octet out of range ' ) : _par ( ' ' ) ; var pfx = s . slice ( slash + 1 ) ; if ( ! pfx ) return _par ( ' ' ) ; if ( / [ ^ 0 - 9 ] / . test ( pfx ) ) return _err ( ' Invalid character ' ) ; var n = parseInt ( pfx , 10 ) ; return ( n > = 0 & & n < = 32 ) ? _ok ( ) : _err ( ' Prefix must be 0-32 ' ) ; }
if ( flag == = F_SUBNET ) { if ( ! arg1 ) return _par ( ' ' ) ; var prefix = parseInt ( arg1 , 10 ) ; if ( isNaN ( prefix ) | | prefix < 1 | | prefix > 30 ) return _err ( ' Prefix must be 1-30 ' ) ; var r = _ipv4 ( s ) ; if ( r != = ' ok ' ) return ( r == = ' partial ' | | r == = ' empty ' ) ? _par ( ' ' ) : ( r == = ' badchar ' ? _err ( ' Invalid character ' ) : _err ( ' Invalid format ' ) ) ; var mB = prefix == = 0 ? 0 : ( ( 0xFFFFFFFF << ( 32 - prefix ) ) >> > 0 ) ; var ipN = s . split ( ' . ' ) . reduce ( function ( a , o ) { return ( a << 8 | + o ) >> > 0 ; } , 0 ) ; return ( ( ipN & ( ~ mB >> > 0 ) ) != = 0 ) ? _err ( ' Host bits must be zero ' ) : _ok ( ) ; }
if ( flag == = F_ADDR ) { var r = _ipv4 ( s ) ; if ( r != = ' ok ' ) return ( r == = ' partial ' | | r == = ' empty ' ) ? _par ( ' ' ) : ( r == = ' badchar ' ? _err ( ' Invalid character ' ) : _err ( ' Invalid format ' ) ) ; if ( ! arg1 | | ! arg2 ) return _par ( ' ' ) ; var prefix = parseInt ( arg1 , 10 ) ; if ( isNaN ( prefix ) | | prefix < 1 | | prefix > 30 ) return _par ( ' ' ) ; if ( _ipv4 ( arg2 ) != = ' ok ' ) return _par ( ' ' ) ; var mB = prefix == = 0 ? 0 : ( ( 0xFFFFFFFF << ( 32 - prefix ) ) >> > 0 ) ; var snN = arg2 . split ( ' . ' ) . reduce ( function ( a , o ) { return ( a << 8 | + o ) >> > 0 ; } , 0 ) ; if ( ( snN & ( ~ mB >> > 0 ) ) != = 0 ) return _par ( ' ' ) ; var iPts = s . split ( ' . ' ) . map ( Number ) , sPts = arg2 . split ( ' . ' ) . map ( Number ) ; var ipN = ( ( iPts [ 0 ] << 24 ) | ( iPts [ 1 ] << 16 ) | ( iPts [ 2 ] << 8 ) | iPts [ 3 ] ) >> > 0 , sN = ( ( sPts [ 0 ] << 24 ) | ( sPts [ 1 ] << 16 ) | ( sPts [ 2 ] << 8 ) | sPts [ 3 ] ) >> > 0 ; if ( ( ipN & mB ) != = ( sN & mB ) ) return _err ( ' IP not in VLAN subnet ' ) ; var hM = ( ~ mB ) >> > 0 , netN = ( sN & mB ) >> > 0 ; if ( ipN == = netN ) return _err ( ' Network address not allowed ' ) ; if ( ipN == = ( netN | hM ) >> > 0 ) return _err ( ' Broadcast address not allowed ' ) ; return _ok ( ) ; }
return _par ( ' ' ) ;
}
function _checkLine ( s ) {
var anyPartial = false , firstMsg = ' ' ;
var flags = [ F_IPV4 , F_IPV6 , F_SUBNET , F_ADDR , F_MAC , F_URL , F_PORT , F_DASH , F_NET , F_DOMAIN , F_T24H , F_RNGINT , F_ENDPT , F_IPV4C ] ;
for ( var i = 0 ; i < flags . length ; i + + ) {
if ( ! ( validation & flags [ i ] ) ) continue ;
var r = _checkFlag ( s , flags [ i ] ) ;
if ( r . ok ) return r ;
if ( r . partial ) anyPartial = true ;
else if ( ! firstMsg ) firstMsg = r . msg ;
}
return anyPartial ? _par ( ' ' ) : _err ( firstMsg | | ' Invalid ' ) ;
}
var lines = value . split ( ' \n ' ) , hasPartial = false , seen = { } , hasContent = false ;
for ( var i = 0 ; i < lines . length ; i + + ) {
var l = lines [ i ] . trim ( ) ;
if ( ! l ) continue ;
hasContent = true ;
var r = _checkLine ( l ) ;
if ( ! r . ok & & ! r . partial ) return r ;
if ( ! r . ok ) { hasPartial = true ; continue ; }
if ( dedup ) { if ( seen [ l ] ) return _err ( ' Duplicate entry ' ) ; seen [ l ] = true ; }
}
if ( ! hasContent ) return _par ( ' ' ) ;
if ( hasPartial ) return _par ( ' ' ) ;
return _ok ( ) ; """
return f ' function bigValidate(value,validation,collisions,dedup,arg1,arg2) {{ { decls } { body } \n }} '
2026-05-28 22:50:00 -04:00
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 ====================================================
2026-05-30 14:57:33 -04:00
def build_snap_val ( changes ) :
""" Return a brief summary of changed field names for the history table cell. """
if not changes :
2026-05-28 22:50:00 -04:00
return ' '
2026-05-30 14:57:33 -04:00
fields = [ c [ ' field ' ] for c in changes ]
if len ( fields ) < = 2 :
return e ( ' , ' . join ( fields ) )
return e ( f ' { fields [ 0 ] } , { fields [ 1 ] } (+ { len ( fields ) - 2 } more) ' )
def snap_expand_row ( changes , colspan ) :
""" Return a hidden <tr> with a per-field change table. """
if not changes :
2026-05-28 22:50:00 -04:00
return ' '
2026-05-30 14:57:33 -04:00
rows = ' '
for c in changes :
bval = c [ ' before ' ] if c [ ' before ' ] is not None else ' '
aval = c [ ' after ' ] if c [ ' after ' ] is not None else ' '
rows + = (
' <tr> '
f ' <td class= " snap-expand-field " > { e ( c [ " field " ] ) } </td> '
f ' <td class= " snap-expand-val " > { e ( bval ) if bval else " <em>(none)</em> " } </td> '
f ' <td class= " snap-expand-val " > { e ( aval ) if aval else " <em>(none)</em> " } </td> '
' </tr> '
2026-05-28 22:50:00 -04:00
)
2026-05-30 14:57:33 -04:00
inner = (
' <table class= " snap-expand-table " > '
' <thead><tr> '
' <th class= " snap-expand-th " >Field</th> '
' <th class= " snap-expand-th " >Before</th> '
' <th class= " snap-expand-th " >After</th> '
' </tr></thead> '
f ' <tbody> { rows } </tbody> '
' </table> '
)
2026-05-28 22:50:00 -04:00
return f ' <tr hidden><td colspan= " { colspan } " class= " snap-expand-cell " > { inner } </td></tr> '
# 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 <script> for a form ' s validation and submit-gate wiring. """
safe = re . compile ( r ' ^[a-zA-Z0-9_-]+$ ' )
lines = [ ' (function() { ' ]
lines . append ( " var _prev = document.currentScript.previousElementSibling; " )
lines . append ( " var _card = _prev.closest( ' .card ' ) || _prev.parentElement; " )
lines . append ( f " var _submit = _card ? _card.querySelector( ' { submit_sel } ' ) : null; " )
lines . append ( ' ' )
subnet_items = [ ]
validate_items = [ ]
checkbox_only = [ ]
gate_vars = [ ]
for spec in field_specs :
t = spec . get ( ' type ' , ' ' )
if t == ' subnet_row ' :
sn = spec . get ( ' subnet_name ' , ' subnet ' )
pn = spec . get ( ' prefix_name ' , ' subnet_mask ' )
if not ( safe . match ( sn ) and safe . match ( pn ) ) :
continue
sv = ' _ ' + sn . replace ( ' - ' , ' _ ' )
pv = ' _ ' + pn . replace ( ' - ' , ' _ ' )
lines . append ( f " var { sv } = _card.querySelector( ' [name= \" { sn } \" ] ' ); " )
lines . append ( f " var { pv } = _card.querySelector( ' [name= \" { pn } \" ] ' ); " )
subnet_items . append ( ( sv , pv ) )
gate_vars . append ( f ' { sv } && { sv } ._valid ' )
elif t == ' field ' :
nm = spec . get ( ' name ' , ' ' )
itype = spec . get ( ' input_type ' , ' text ' )
if not nm or not safe . match ( nm ) :
continue
vn = ' _ ' + nm . replace ( ' - ' , ' _ ' )
lines . append ( f " var { vn } = _card.querySelector( ' [name= \" { nm } \" ] ' ); " )
if itype == ' checkbox ' :
if spec . get ( ' validate ' ) :
validate_items . append ( ( vn , nm ) )
gate_vars . append ( f ' { vn } && { vn } ._valid ' )
else :
checkbox_only . append ( vn )
else :
validate_items . append ( ( vn , nm ) )
gate_vars . append ( f ' { vn } && { vn } ._valid ' )
lines . append ( ' ' )
gate_expr = ' && ' . join ( gate_vars ) if gate_vars else ' true '
lines . append ( ' function _upd() { ' )
lines . append ( ' if (!_submit) return; ' )
lines . append ( f ' _submit.disabled = !( { gate_expr } ); ' )
lines . append ( ' } ' )
lines . append ( ' ' )
for vn , _ in validate_items :
lines . append ( f " if ( { vn } ) { vn } .addEventListener( ' input ' , function() {{ validateEl( { vn } ); _upd(); }} ); " )
for sv , pv in subnet_items :
lines . append ( f ' function _chkSubnet() {{ ' )
lines . append ( f ' if (! { sv } || ! { pv } ) return; ' )
2026-05-31 00:22:39 -04:00
lines . append ( f " var res = bigValidate( { sv } .value.trim(), { parse_validation ( ' VALIDATION_SUBNET ' ) } , null, false, { pv } .value.trim(), null); " )
2026-05-28 22:50:00 -04:00
lines . append ( f " setFieldHint( { sv } , res.ok ? ' ' : (res.msg|| ' ' ), res.ok ? ' ok ' : (res.partial ? ' warning ' : ' error ' )); " )
lines . append ( f ' { sv } ._valid = res.ok; ' )
lines . append ( f " var dot = { pv } .closest( ' .form-group ' ).querySelector( ' .subnet-dotted ' ); " )
lines . append ( f ' var n = parseInt( { pv } .value, 10); ' )
lines . append ( f " if (dot) dot.textContent = (!isNaN(n) && n >= 1 && n <= 30) ? prefixToDotted(n) : ' ' ; " )
lines . append ( f ' _upd(); ' )
lines . append ( f ' }} ' )
lines . append ( f " if ( { sv } ) { sv } .addEventListener( ' input ' , _chkSubnet); " )
lines . append ( f " if ( { pv } ) { pv } .addEventListener( ' input ' , _chkSubnet); " )
for vn in checkbox_only :
lines . append ( f " if ( { vn } ) { vn } .addEventListener( ' change ' , _upd); " )
lines . append ( ' }()); ' )
return ' <script> ' + ' \n ' . join ( lines ) + ' </script> '
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 < td > . . . < / td > 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 < tr > ( 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 ' <tr class= " table-picker-row { sel_cls } " { attrs } > { cells_html } </tr> '
thead = ' ' . join ( f ' <th> { e ( h ) } </th> ' for h in headers )
table_html = (
' <div class= " table-wrapper " > '
' <table class= " data-table table-picker-table " > '
f ' <thead><tr> { thead } </tr></thead> '
f ' <tbody> { rows_html } </tbody> '
' </table></div> '
)
btn_label = f ' <span class= " table-picker-name " > { e ( value ) if value else " Select... " } </span> '
btn_badge = ' '
if cur_row and cur_row . get ( ' badge_class ' ) :
btn_badge = (
f ' <span class= " badge { e ( cur_row [ " badge_class " ] ) } table-picker-badge " > '
f ' { e ( cur_row . get ( " badge_label " , " " ) ) } </span> '
)
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 ' <th> { e ( c [ " label " ] ) } </th> ' for c in summary_config )
def _dcell ( c ) :
cls = ' class= " col-mono " ' if c . get ( ' mono ' ) else ' '
return f ' <td { cls } > { e ( str ( summary . get ( c [ " field " ] ) or " - " ) ) } </td> '
dcells = ' ' . join ( _dcell ( c ) for c in summary_config )
ext_meta = (
' <table class= " table-picker-stats " > '
f ' <thead><tr> { hcells } </tr></thead> '
f ' <tbody><tr> { dcells } </tr></tbody> '
' </table> '
)
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 ) } " '
2026-05-29 22:16:56 -04:00
script = (
' <script>(function() { '
' var _fg=document.currentScript.previousElementSibling; '
' var _pk=_fg.querySelector( \' .table-picker \' ); '
' var _btn=_pk.querySelector( \' .table-picker-btn \' ); '
' var _hdr=_pk.querySelector( \' .table-picker-header \' ); '
' var _dd=_pk.querySelector( \' .table-picker-dropdown \' ); '
' var _hid=_pk.querySelector( \' input[type= " hidden " ] \' ); '
' var _sc=JSON.parse(_pk.dataset.summary|| \' [] \' ); '
' function _apply(key) { '
' var row=_dd.querySelector( \' .table-picker-row[data-key= " \' +key+ \' " ] \' ); '
' if(!row)return; '
' _btn.querySelector( \' .table-picker-name \' ).textContent=row.dataset.label||key; '
' var badge=_btn.querySelector( \' .table-picker-badge \' ); '
' if(row.dataset.badgeClass) { '
' if(!badge) { badge=document.createElement( \' span \' );_btn.appendChild(badge);} '
' badge.className= \' badge \' +row.dataset.badgeClass+ \' table-picker-badge \' ; '
' badge.textContent=row.dataset.badgeLabel|| \' \' ; '
' }else if(badge) { badge.remove();} '
' if(_sc.length) { '
' var stats=_hdr.querySelector( \' .table-picker-stats \' ); '
' if(!stats) { '
' stats=document.createElement( \' table \' ); '
' stats.className= \' table-picker-stats \' ; '
' var hc=_sc.map(function(c) { return \' <th> \' +htmlEsc(c.label)+ \' </th> \' ;}).join( \' \' ); '
' stats.innerHTML= \' <thead><tr> \' +hc+ \' </tr></thead><tbody><tr></tr></tbody> \' ; '
' _hdr.appendChild(stats); '
' } '
' var dc=_sc.map(function(c) { '
' var v=row.dataset[c.field]!==undefined?row.dataset[c.field]: \' - \' ; '
' return \' <td \' +(c.mono? \' class= " col-mono " \' : \' \' )+ \' > \' +htmlEsc(v)+ \' </td> \' ; '
' }).join( \' \' ); '
' stats.querySelector( \' tbody tr \' ).innerHTML=dc; '
' } '
' _dd.querySelectorAll( \' .table-picker-row \' ).forEach(function(r) { '
' r.classList.toggle( \' selected \' ,r===row); '
' }); '
' } '
' _hid.addEventListener( \' change \' ,function() { _apply(_hid.value);}); '
' _btn.addEventListener( \' click \' ,function(e) { '
' e.stopPropagation(); '
' var wasOpen=_dd.classList.contains( \' open \' ); '
' tablePickerCloseAll(); '
' if(!wasOpen)_dd.classList.add( \' open \' ); '
' }); '
' _dd.addEventListener( \' click \' ,function(e) { e.stopPropagation();}); '
' _dd.querySelectorAll( \' .table-picker-row \' ).forEach(function(row) { '
' row.addEventListener( \' click \' ,function() { '
' _hid.value=this.dataset.key; '
' tablePickerCloseAll(); '
' _hid.dispatchEvent(new Event( \' change \' , { bubbles:true})); '
' }); '
' }); '
' })();</script> '
)
2026-05-28 22:50:00 -04:00
return (
' <div class= " form-group " > '
f ' <label class= " form-label " > { e ( label ) } </label> '
f ' <div class= " table-picker " { summary_attr } > '
f ' <input type= " hidden " name= " { e ( name ) } " value= " { e ( value ) } " /> '
' <div class= " table-picker-header " > '
f ' <button type= " button " class= " table-picker-btn " > { btn_label } { btn_badge } </button> '
f ' { ext_meta } '
f ' { action_btn_html } '
' </div> '
f ' <div class= " table-picker-dropdown " > { table_html } </div> '
' </div> '
' </div> '
2026-05-29 22:16:56 -04:00
f ' { script } '
2026-05-28 22:50:00 -04:00
)
# 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 ' <p class= " form-hint " > { hint } </p> ' if hint else ' '
readonly = ' readonly ' if item . get ( ' readonly ' ) else ' '
if input_type == ' hidden ' :
return f ' <input type= " hidden " name= " { name } " value= " { e ( value ) } " /> '
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 ' <label class= " form-label " > { label } </label> ' if label else ' '
return (
' <div class= " form-group " > '
f ' { label_html } '
' <label class= " form-checkbox-row " > '
f ' <input type= " checkbox " name= " { name } " { checked } class= " form-checkbox " /> '
f ' <span class= " form-checkbox-label " > { e ( cb_label ) } </span> '
f ' </label> { hint_html } </div> '
)
return (
' <div class= " form-group " > '
' <label class= " form-label " > '
f ' <input type= " checkbox " name= " { name } " { checked } class= " form-checkbox " /> { label } '
f ' </label> { hint_html } </div> '
)
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 (
' <label class= " checkbox-group-item " > '
f ' <input type= " checkbox " name= " { name } " value= " { e ( o . get ( " value " , " " ) ) } " '
f ' { " checked " if o . get ( " value " ) in selected else " " } /> { e ( o . get ( " label " , " " ) ) } '
' </label> '
for o in opts
)
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <div class= " checkbox-group " > { boxes } </div> { hint_html } </div> '
)
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 ' <option value= " { e ( o [ " value " ] ) } " { " selected " if o [ " value " ] == current else " " } > { e ( o [ " label " ] ) } </option> '
for o in options
)
2026-05-31 00:22:39 -04:00
validate_raw = item . get ( ' validate ' , ' ' )
2026-05-28 22:50:00 -04:00
depends = item . get ( ' depends ' , [ ] )
2026-05-31 00:22:39 -04:00
_vmask = parse_validation ( validate_raw ) if validate_raw else 0
validate_attr = f ' data-validate= " { _vmask } " ' if _vmask else ' '
2026-05-28 22:50:00 -04:00
depends_attr = f ' data-depends= " { e ( " , " . join ( depends ) ) } " ' if depends else ' '
2026-05-31 00:22:39 -04:00
if _vmask :
2026-05-28 22:50:00 -04:00
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <div class= " field-wrap " ><select name= " { name } " class= " form-select " { validate_attr } { depends_attr } > { opts_html } </select> '
f ' <p class= " form-hint field-dyn-hint hidden " ></p></div> '
f ' { hint_html } </div> '
)
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <select name= " { name } " class= " form-select " { validate_attr } { depends_attr } > { opts_html } </select> '
f ' { hint_html } </div> '
)
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 ' '
2026-05-31 00:22:39 -04:00
validate_raw = item . get ( ' validate ' , ' VALIDATION_RANGE_INT ' )
2026-05-28 22:50:00 -04:00
depends = item . get ( ' depends ' , [ ] )
existing_ids = apply_tokens ( item . get ( ' existing_ids ' , ' ' ) , tokens )
2026-05-31 00:22:39 -04:00
_vmask = parse_validation ( validate_raw )
validate_attr = f ' data-validate= " { _vmask } " '
2026-05-28 22:50:00 -04:00
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 = ' <p class= " form-hint field-dyn-hint hidden " ></p> '
inp = (
f ' <input type= " number " name= " { name } " value= " { e ( value ) } " { min_attr } { max_attr } '
f ' class= " form-input form-input-mono " { readonly } { validate_attr } { depends_attr } { existing_attr } /> '
)
if item . get ( ' layout ' ) == ' inline ' :
return (
' <div class= " form-group " style= " display:flex;align-items:center;gap:0.75em " > '
f ' <label class= " form-label " style= " margin:0;white-space:nowrap " > { label } </label> '
f ' <div class= " field-wrap " style= " width:6rem " > { inp } { dyn_hint_html } </div> '
f ' { hint_html } </div> '
)
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <div class= " field-wrap " > { inp } { dyn_hint_html } </div> { hint_html } </div> '
)
if input_type == ' textarea ' :
rows = item . get ( ' rows ' , 4 )
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <textarea name= " { name } " rows= " { rows } " placeholder= " { placeholder } " '
f ' class= " form-input " > { e ( value ) } </textarea> '
f ' { hint_html } </div> '
)
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 ' <td class= " col-mono " > { e ( iname ) } </td> ' ,
f ' <td> { e ( type_txt ) } </td> ' ,
f ' <td><span class= " badge { sc } " > { st } </span></td> ' ,
f ' <td> { e ( carrier_txt ) } </td> ' ,
f ' <td> { e ( disp_speed ) } </td> ' ,
f ' <td> { e ( disp_mtu ) } </td> ' ,
f ' <td class= " col-mono " > { e ( raw_mac or " - " ) } </td> ' ,
] ,
' 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 = (
' <button type= " button " class= " btn btn-secondary iface-configure-btn " '
f ' data-iface= " { e ( iname ) } " data-mtu= " { e ( raw_mtu or " " ) } " '
f ' data-mac= " { e ( raw_mac or " " ) } " data-perm-mac= " { e ( perm_mac ) } " '
f ' data-min-mtu= " { str ( min_mtu ) if min_mtu is not None else " " } " '
f ' data-max-mtu= " { str ( max_mtu ) if max_mtu is not None else " " } " > '
' Configure</button> '
)
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 )
2026-05-31 00:22:39 -04:00
validate_raw = item . get ( ' validate ' , ' ' )
2026-05-28 22:50:00 -04:00
depends = item . get ( ' depends ' , [ ] )
2026-05-31 00:22:39 -04:00
_vmask = parse_validation ( validate_raw ) if validate_raw else 0
validate_attr = f ' data-validate= " { _vmask } " ' if _vmask else ' '
2026-05-28 22:50:00 -04:00
depends_attr = f ' data-depends= " { e ( " , " . join ( depends ) ) } " ' if depends else ' '
2026-05-31 00:22:39 -04:00
if _vmask :
2026-05-28 22:50:00 -04:00
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <div class= " field-wrap " ><input type= " { e ( input_type ) } " name= " { name } " value= " { e ( value ) } " '
f ' placeholder= " { placeholder } " class= " form-input " { readonly } { validate_attr } { depends_attr } /> '
f ' <p class= " form-hint field-dyn-hint hidden " ></p></div> '
f ' { hint_html } </div> '
)
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <input type= " { e ( input_type ) } " name= " { name } " value= " { e ( value ) } " '
f ' placeholder= " { placeholder } " class= " form-input " { readonly } { validate_attr } { depends_attr } /> '
f ' { hint_html } </div> '
)
# 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 ' <p class= " form-hint " > { hint } </p> ' if hint else ' '
2026-05-31 00:22:39 -04:00
validate_raw = item . get ( ' validate ' , ' ' )
_vmask = parse_validation ( validate_raw ) if validate_raw else 0
2026-05-28 22:50:00 -04:00
try :
items_list = json . loads ( apply_tokens ( item . get ( ' items ' , ' [] ' ) , tokens ) )
except Exception :
items_list = [ ]
rows = ' ' . join (
' <div class= " editable-list-item " > '
f ' <input type= " text " name= " { name } " value= " { e ( v ) } " class= " form-input " /> '
' <button type= " button " class= " btn btn-ghost btn-sm editable-list-remove " >Remove</button> '
' </div> '
for v in items_list
)
2026-05-31 00:22:39 -04:00
validate_attr = f ' data-validate= " { _vmask } " ' if _vmask else ' '
2026-05-28 22:50:00 -04:00
return (
f ' <div class= " form-group " ><label class= " form-label " > { label } </label> '
f ' <div class= " editable-list " data-name= " { name } " data-placeholder= " { ph } " { validate_attr } > '
f ' { rows } '
f ' <button type= " button " class= " btn btn-ghost btn-sm editable-list-add " >+ { add_lbl } </button> '
f ' </div> { hint_html } </div> '
)
# Table worker script =================================================
def build_table_worker_script ( item , expanded_ra_fields ) :
""" Emit a <script> registering a table worker for any non-standard inline_edit field types.
Returns empty string when all fields are standard types . """
if not expanded_ra_fields :
return ' '
worker_id = get_worker_id ( item . get ( ' datasource ' , ' ' ) )
if not worker_id :
return ' '
nonstandard = set ( )
for fields in expanded_ra_fields . values ( ) :
for f in fields :
it = f . get ( ' input_type ' , ' text ' )
if it not in STANDARD_INPUT_TYPES :
nonstandard . add ( it )
if not nonstandard :
return ' '
if nonstandard == { ' credentials ' } :
return (
f ' <script>registerTableWorker( { js_str ( worker_id ) } , (function() {{ \n '
' function _buildCreds(provider, data) { \n '
' if (provider === \' noip \' ) { \n '
' return \' <div class= " cred-field " ><span class= " cred-label " >U:</span> \' + \n '
' \' <input type= " text " name= " username " value= " \' + htmlEsc(data.username|| \' \' ) '
' + \' " class= " form-input inline-edit-input " /></div> \' + \n '
' \' <div class= " cred-field " ><span class= " cred-label " >P:</span> \' + \n '
' \' <input type= " password " name= " password " value= " \' + htmlEsc(data.password|| \' \' ) '
' + \' " class= " form-input inline-edit-input " /></div> \' ; \n '
' } \n '
' return \' <input type= " text " name= " api_token " value= " \' + htmlEsc(data.api_token|| \' \' ) '
' + \' " class= " form-input inline-edit-input " placeholder= " API Token " /> \' ; \n '
' } \n '
' return { \n '
' renderCell: function(fDef, td, val, row) { \n '
' if (fDef.input_type !== \' credentials \' ) return false; \n '
' td.innerHTML = _buildCreds(row.provider || \' noip \' , row); \n '
' return true; \n '
' }, \n '
' afterRowOpen: function(tr, row) { \n '
' var provSel = tr.querySelector( \' td[data-field= " provider " ] select \' ); \n '
' var credTd = tr.querySelector( \' td[data-field= " credentials " ] \' ); \n '
' if (!provSel || !credTd) return; \n '
' provSel.addEventListener( \' change \' , function() { \n '
' credTd.innerHTML = _buildCreds(this.value, row); \n '
' }); \n '
' } \n '
' }; \n '
' }()));</script> \n '
)
return f ' <script>registerTableWorker( { js_str ( worker_id ) } , {{ }} );</script> \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 ' <td { " " . join ( parts ) } > ' if parts else ' <td> '
if not render_fn :
return f ' { td_open } { e ( value ) } </td> '
if render_fn == ' badge_enabled_disabled ' :
if str ( value ) . lower ( ) in ( ' true ' , ' 1 ' , ' yes ' , ' enabled ' ) :
inner = ' <span class= " badge badge-enabled " >Enabled</span> '
else :
inner = ' <span class= " badge badge-disabled " >Disabled</span> '
return f ' { td_open } { inner } </td> '
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 ' <span class= " badge badge-enabled " { tip } >Yes</span> '
else :
tip = f ' data-tooltip= " { e ( opts [ " title_false " ] ) } " ' if opts . get ( ' title_false ' ) else ' '
inner = f ' <span class= " badge badge-disabled " { tip } >No</span> '
return f ' { td_open } { inner } </td> '
if render_fn == ' badge_recording_on_off ' :
if str ( value ) . lower ( ) in ( ' true ' , ' 1 ' , ' yes ' ) :
inner = ' <span class= " badge badge-enabled " >Recording On</span> '
else :
inner = ' <span class= " badge badge-disabled " >Recording Off</span> '
return f ' { td_open } { inner } </td> '
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 ' <form method= " post " action= " { e ( toggle_action ) } " class= " form-inline " > '
f ' <input type= " hidden " name= " row_index " value= " { row_idx } " /> '
' <button type= " submit " class= " btn-badge " > '
f ' <span class= " badge { badge_cls } " > { label } </span></button></form> '
)
else :
inner = f ' <span class= " badge { badge_cls } " > { label } </span> '
return f ' { td_open } { inner } </td> '
if render_fn == ' badge_active_inactive ' :
badges = { ' active ' : ' badge-enabled ' , ' pending ' : ' badge-warning ' }
cls = badges . get ( value . lower ( ) , ' badge-disabled ' )
return f ' { td_open } <span class= " badge { cls } " > { e ( value . title ( ) ) } </span></td> '
if render_fn == ' raw_html ' :
return f ' { td_open } { value } </td> '
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 ' <span class= " tag " data-tooltip= " { e ( tooltip ) } " > '
f ' <span class= " tl-full " > { e ( s ) } </span> '
f ' <span class= " tl-short " > { e ( short ) } </span> '
f ' <span class= " tl-min " > { e ( mini ) } </span> '
' </span> '
)
tags = ' ' . join ( _tag ( t ) for t in items )
return f ' { td_open } <div class= " tag-list " > { tags } </div></td> '
if render_fn == ' interface_status ' :
v = value . upper ( )
if v == ' INVALID ' :
inner = ' <span class= " badge badge-danger " >Invalid</span> '
elif v == ' UP ' :
inner = ' <span class= " badge badge-enabled " >Up</span> '
elif v == ' DOWN ' :
inner = ' <span class= " badge badge-warning " >Down</span> '
else :
inner = f ' <span class= " badge badge-disabled " > { e ( value . title ( ) ) } </span> '
return f ' { td_open } { inner } </td> '
return f ' { td_open } { e ( value ) } </td> '
# 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 ' <div class= " table-toolbar " > { t_inner } </div> '
thead = ' ' . join (
f ' <th class= " { e ( c [ " class " ] ) } " > { e ( c . get ( " label " , " " ) ) } </th> ' if c . get ( " class " ) else f ' <th> { e ( c . get ( " label " , " " ) ) } </th> '
for c in columns
)
if row_actions :
thead + = ' <th></th> '
expanded_ra_fields = {
2026-05-31 00:22:39 -04:00
i : _encode_field_validations ( expand_fields ( ra . get ( ' fields ' , [ ] ) , tokens ) )
2026-05-28 22:50:00 -04:00
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 ' <tr><td colspan= " { colspan } " class= " table-empty " > { empty } </td></tr> '
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 ' <button type= " button " class= " btn { cls } " disabled> { text } </button> '
continue
btns + = (
f ' <form method= " post " action= " { action } " class= " form-inline " > '
f ' <input type= " hidden " name= " row_index " value= " { idx } " /> '
f ' <input type= " hidden " name= " config_hash " value= " { e ( hash_val ) } " /> '
f ' <button type= " submit " class= " btn { cls } " > { text } </button></form> '
)
elif method == ' js_edit ' :
target = e ( ra . get ( ' target ' , ' edit-form ' ) )
row_json = e ( json . dumps ( row ) )
btns + = (
f ' <button type= " button " class= " btn { cls } row-edit-btn " '
f ' data-edit-mode= " reveal " '
f ' data-row-index= " { idx } " data-row= " { row_json } " '
f ' data-target= " { target } " > { text } </button> '
)
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 ' <button type= " button " class= " btn { cls } row-edit-btn " '
f ' data-edit-mode= " inline " '
f ' data-row-index= " { idx } " data-row= " { row_json } " '
f ' data-action= " { action } " data-fields= " { fields_json } " { worker_attr } > { text } </button> '
)
else :
btns + = f ' <a href= " { action } ?row_index= { idx } " class= " btn { cls } " > { text } </a> '
cells + = f ' <td class= " col-actions " > { btns } </td> '
tbody + = f ' <tr> { cells } </tr> '
worker_script = build_table_worker_script ( item , expanded_ra_fields )
return (
f ' { toolbar_html } '
' <div class= " table-wrapper " > '
' <table class= " data-table " > '
f ' <thead><tr> { thead } </tr></thead> '
f ' <tbody> { tbody } </tbody> '
f ' </table></div> { 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 ' <h1> { e ( apply_tokens ( item . get ( " text " , " " ) , tokens ) ) } </h1> '
if t == ' hr ' :
return ' <hr class= " divider " /> '
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 ' <p> { text } <a href= " { href } " class= " auth-link " > { ltext } </a></p> '
return f ' <p> { text } </p> '
if t == ' spacer ' :
return ' <div class= " spacer " ></div> '
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 ' <button type= " submit " class= " btn { e ( cls ) } " formaction= " { formaction } " { disabled } > { text } </button> '
if item . get ( ' method ' , ' ' ) . lower ( ) == ' post ' :
return (
f ' <form method= " post " action= " { action } " class= " form-inline " > '
f ' <button type= " submit " class= " btn { e ( cls ) } " { disabled } > { text } </button></form> '
)
if action_raw :
return f ' <a href= " { action } " class= " btn { e ( cls ) } " > { text } </a> '
return f ' <button type= " submit " class= " btn { e ( cls ) } " { disabled } > { text } </button> '
if t == ' button_cancel ' :
text = e ( apply_tokens ( item . get ( ' text ' , ' Cancel ' ) , tokens ) )
extra_cls = ( ' ' + item [ ' class ' ] ) if item . get ( ' class ' ) else ' '
return f ' <button type= " button " class= " btn btn-secondary btn-cancel { extra_cls } " disabled> { text } </button> '
if t == ' header_page_title ' :
return f ' <div class= " page-header " > { build_items ( item . get ( " items " , [ ] ) , tokens , req ) } </div> '
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 ) } </ { tag } > '
if t == ' auth_card ' :
return f ' <div class= " auth-card " > { build_items ( item . get ( " items " , [ ] ) , tokens , req ) } </div> '
if t == ' stat_card_grid ' :
return f ' <div class= " stat-card-grid " > { build_items ( item . get ( " items " , [ ] ) , tokens , req ) } </div> '
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 ' <div class= " { cls } " > '
f ' <div class= " stat-card-label " > { label } </div> '
' <div class= " stat-card-value-row " > '
f ' <span class= " stat-card-value " > { value } </span> '
' <button type= " button " class= " btn btn-ghost btn-sm " '
f ' data-reveal-card= " { e ( reveal_card_id ) } " >Edit</button> '
' </div> '
f ' <div class= " stat-card-sub " > { sub } </div> '
' </div> '
)
if edit_action and edit_field :
min_attr = f ' min= " { e ( edit_min ) } " ' if edit_min else ' '
suffix_html = f ' <span> { e ( edit_suffix ) } </span> ' if edit_suffix else ' '
input_wrap = (
' <div class= " stat-card-value-row " > '
f ' <input type= " { e ( edit_input_type ) } " name= " { e ( edit_field ) } " value= " { e ( edit_raw ) } " '
f ' data-original= " { e ( edit_raw ) } " class= " form-input stat-card-edit-input " { min_attr } /> '
f ' { suffix_html } </div> '
)
return (
f ' <div class= " { cls } stat-card-editable " > '
f ' <div class= " stat-card-label " > { label } </div> '
' <div class= " stat-card-view " > '
f ' <span class= " stat-card-value " > { value } </span> '
' <button type= " button " class= " btn btn-ghost btn-sm stat-card-edit-btn " >Edit</button> '
' </div> '
f ' <form class= " stat-card-edit-form hidden " action= " { e ( edit_action ) } " method= " post " > '
f ' <input type= " hidden " name= " config_hash " value= " { e ( config_hash ( ) ) } " /> '
f ' { input_wrap } '
' <div class= " stat-card-edit-actions " > '
' <button type= " submit " class= " btn btn-primary btn-sm " disabled>Save</button> '
' <button type= " button " class= " btn btn-secondary btn-sm stat-card-cancel-btn " >Cancel</button> '
' </div> '
' </form> '
f ' <div class= " stat-card-sub " > { sub } </div> '
' </div> '
)
return (
f ' <div class= " { cls } " > '
f ' <div class= " stat-card-label " > { label } </div> '
f ' <div class= " stat-card-value " > { value } </div> '
f ' <div class= " stat-card-sub " > { sub } </div> '
' </div> '
)
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 ' <div class= " card-header " ><h2 class= " card-title " > { e ( label ) } </h2></div> ' if label else ' '
body = build_items ( item . get ( ' items ' , [ ] ) , tokens , req )
return f ' <div class= " card { cls_hidden } " { id_attr } > { header } <div class= " card-body " > { body } </div></div> '
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 (
' <div class= " form-group " > '
f ' <label class= " form-label " > { label } </label> '
f ' <div class= " field-status-badge " ><span class= " badge { badge_cls } " > { badge_text } </span></div> '
' </div> '
)
if t == ' info_bar ' :
variant = item . get ( ' variant ' , ' info ' )
text = e ( apply_tokens ( item . get ( ' text ' , ' ' ) , tokens ) )
return f ' <div class= " info-bar info-bar-inline info-bar- { e ( variant ) } " > { text } </div> '
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 ' <pre class= " pre-block " { extra } > { text } </pre> '
if t == ' credential_fields ' :
psel = e ( item . get ( ' provider_select ' , ' provider ' ) )
return (
f ' <div class= " credential-fields " data-provider-select= " { psel } " > '
' <div class= " cred-group-token hidden " > '
' <div class= " form-group " ><label class= " form-label " >API Token</label> '
' <input type= " text " name= " api_token " class= " form-input " /></div> '
' </div> '
' <div class= " cred-group-noip hidden " > '
' <div class= " form-group " ><label class= " form-label " >Username</label> '
' <input type= " text " name= " username " class= " form-input " /></div> '
' <div class= " form-group " ><label class= " form-label " >Password</label> '
' <input type= " password " name= " password " class= " form-input " /></div> '
' </div> '
' </div> '
)
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 ' <div class= " info-grid-row " > { cells } </div> '
return f ' <div class= " info-grid " > { rows_html } </div> '
if t == ' grid_label ' :
return f ' <div class= " info-grid-label " > { e ( apply_tokens ( item . get ( " text " , " " ) , tokens ) ) } </div> '
if t == ' grid_value ' :
return f ' <div class= " info-grid-value " > { e ( apply_tokens ( item . get ( " text " , " " ) , tokens ) ) } </div> '
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 ' <input type= " hidden " name= " config_hash " value= " { e ( config_hash ( ) ) } " /> '
originals = collect_form_originals ( item . get ( ' items ' , [ ] ) , tokens )
orig_field = (
f ' <input type= " hidden " name= " original_values " value= " { e ( json . dumps ( originals ) ) } " /> '
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 ' <form action= " { action } " method= " { method } " > { hash_field } { orig_field } { inner } </form> { script } '
if t == ' hidden ' :
name = e ( item . get ( ' name ' , ' ' ) )
value = e ( apply_tokens ( item . get ( ' value ' , ' ' ) , tokens ) )
return f ' <input type= " hidden " name= " { name } " value= " { value } " /> '
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 ' <th> { e ( f . get ( " label " , " " ) ) } </th> ' for f in fields ) + ' <th></th> '
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 '
2026-05-31 00:22:39 -04:00
f_validate_raw = f . get ( ' validate ' , ' ' ) or f . get ( ' valtype ' , ' ' )
f_attrs = f . get ( ' attrs ' , { } )
_vmask = parse_validation ( f_validate_raw ) if f_validate_raw else 0
2026-05-28 22:50:00 -04:00
attr_str = f ' data-field= " { f_name } " data-required= " { f_required } " '
2026-05-31 00:22:39 -04:00
if _vmask :
attr_str + = f ' data-validate= " { _vmask } " '
2026-05-28 22:50:00 -04:00
for ak , av in f_attrs . items ( ) :
attr_str + = f ' { e ( ak ) } = " { e ( str ( av ) ) } " '
inp = f ' <input type= " text " class= " form-input " { attr_str } placeholder= " { f_placeholder } " /> '
2026-05-31 00:22:39 -04:00
if _vmask :
2026-05-28 22:50:00 -04:00
field_inner = (
' <div class= " field-wrap " > '
+ inp +
' <p class= " form-hint field-dyn-hint hidden " ></p> '
' </div> '
)
else :
field_inner = inp
form_rows + = (
f ' <tr> '
f ' <td class= " record-editor-field-label " > { f_label } :</td> '
f ' <td> { field_inner } </td> '
f ' </tr> '
)
return (
f ' <div class= " form-group record-editor " data-name= " { name } " data-empty-message= " { empty } " > '
f ' <div class= " record-editor-body " > '
f ' <div class= " record-editor-table-wrap " > '
f ' <label class= " form-label record-editor-label " > { label } </label> '
f ' <table class= " data-table record-editor-table " > '
f ' <thead><tr> { ths } </tr></thead> '
f ' <tbody class= " record-editor-rows " > '
f ' <tr class= " record-editor-empty-row " > '
f ' <td colspan= " { col_count } " class= " table-empty " > { empty } </td> '
f ' </tr> '
f ' </tbody> '
f ' </table> '
f ' </div> '
f ' <div class= " record-editor-form " > '
f ' <table class= " record-editor-fields-table " ><tbody> { form_rows } </tbody></table> '
f ' <div style= " margin-top:0.5rem " > '
f ' <button type= " button " class= " btn btn-secondary btn-sm record-editor-add-btn " >Add</button> '
f ' <button type= " button " class= " btn btn-ghost btn-sm record-editor-cancel-btn hidden " style= " margin-left:0.5rem " >Cancel</button> '
f ' </div> '
f ' </div> '
f ' </div> '
f ' <input type= " hidden " name= " { name } " class= " record-editor-hidden " value= " [] " /> '
f ' </div> '
)
if t == ' readonly_select ' :
2026-05-29 02:33:07 -04:00
label = e ( item . get ( ' label ' , ' Gateway ' ) )
name = e ( item . get ( ' name ' , ' gateway ' ) )
hint = e ( apply_tokens ( item . get ( ' hint ' , ' ' ) , tokens ) )
hint_html = f ' <p class= " form-hint " > { hint } </p> ' if hint else ' '
2026-05-28 22:50:00 -04:00
return (
f ' <div class= " form-group " > '
f ' <label class= " form-label " > { label } </label> '
f ' <select name= " { name } " class= " form-select readonly-select " disabled> '
f ' <option value= " " >(add identities first)</option> '
f ' </select> '
2026-05-29 02:33:07 -04:00
f ' { hint_html } '
2026-05-28 22:50:00 -04:00
f ' </div> '
)
if t == ' overridable_textarea ' :
label = e ( item . get ( ' label ' , ' ' ) )
name = e ( item . get ( ' name ' , ' ' ) )
override_name = e ( item . get ( ' override_name ' , name + ' _override ' ) )
2026-05-31 00:22:39 -04:00
validate_raw = item . get ( ' validate ' , ' ' )
_vmask = parse_validation ( validate_raw ) if validate_raw else 0
validate_attr = f ' data-validate-lines= " { _vmask } " ' if _vmask else ' '
dyn_hint_html = ' <p class= " form-hint field-dyn-hint hidden " ></p> ' if _vmask else ' '
wrap_open = ' <div class= " field-wrap " > ' if _vmask else ' '
wrap_close = ' </div> ' if _vmask else ' '
2026-05-29 02:33:07 -04:00
hint = e ( apply_tokens ( item . get ( ' hint ' , ' ' ) , tokens ) )
hint_html = f ' <p class= " form-hint " > { hint } </p> ' if hint else ' '
2026-05-28 22:50:00 -04:00
return (
f ' <div class= " form-group " > '
f ' <label class= " form-label override-header " > '
f ' <span> { label } </span> '
f ' <label class= " override-toggle " > '
f ' <input type= " checkbox " name= " { override_name } " class= " form-checkbox override-check " /> Override '
f ' </label> '
f ' </label> '
f ' { wrap_open } '
f ' <textarea name= " { name } " class= " form-input auto-textarea " rows= " 2 " readonly { validate_attr } ></textarea> '
2026-05-29 02:33:07 -04:00
f ' { dyn_hint_html } '
2026-05-28 22:50:00 -04:00
f ' { wrap_close } '
2026-05-29 02:33:07 -04:00
f ' { hint_html } '
2026-05-28 22:50:00 -04:00
f ' </div> '
)
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 ' <div class= " form-row- { cols } " > { inner } </div> '
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 (
' <div class= " form-group " > '
f ' <label class= " form-label " > { subnet_label } </label> '
' <div class= " field-wrap " > '
' <div class= " subnet-row-wrap " > '
f ' <input type= " text " name= " { subnet_name } " value= " { e ( subnet_val ) } " placeholder= " { subnet_ph } " class= " form-input " /> '
' <span class= " subnet-sep " >/</span> '
f ' <input type= " number " name= " { prefix_name } " value= " { pf } " min= " 1 " max= " 30 " class= " form-input subnet-prefix-input " /> '
' </div> '
f ' <span class= " subnet-dotted " > { e ( dotted ) } </span> '
' <p class= " form-hint field-dyn-hint hidden " ></p> '
' </div> '
' </div> '
)
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 ' <select name= " { name } " class= " form-select " { extra } > { options } </select> '
if t == ' spacer ' :
return ' <span style= " margin-left:auto " ></span> '
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 ' <div class= " button-row " { style_attr } > { inner } </div> '
if t == ' table ' :
return build_table ( item , tokens , req )
if t == ' raw_html ' :
return Markup ( apply_tokens ( item . get ( ' html ' , ' ' ) , tokens ) )
return ' '