2026-06-02 12:49:39 -04:00
# factory.py: HTML renderer and shared utilities
# Builds HTML from content.json item trees. Pure rendering - no data fetching.
2026-05-28 22:50:00 -04:00
from flask import session
from markupsafe import Markup
2026-06-02 12:49:39 -04:00
import json , re , sys , html as html_mod , os , subprocess
from config_utils import (
config_hash , load_config , CONFIGS_DIR , DATA_DIR , WWW_DIR , APP_DIR ,
ACCOUNTS_FILE , HEALTH_FILE , BLOCKLISTS_DIR ,
fmt_timestamp , relative_time , fmt_bytes , resolve_iface ,
WEB_APP_DISPLAY_NAME ,
)
from config_utils import (
get_pending_entries , get_dashboard_pending , _find_cmd_in_queues ,
_apply_changes_immediately , _seconds_until_next_run , _format_timing ,
_is_locked , _lock_mtime , _entry_ts_from_queue ,
)
PAGES_DIR = os . path . join ( APP_DIR , ' pages ' )
NAVBAR_FILE = os . path . join ( APP_DIR , ' navbar.json ' )
CSS_FILE = os . path . join ( DATA_DIR , ' styles.css ' )
COMMON_JS_FILE = os . path . join ( DATA_DIR , ' common.js ' )
2026-05-28 22:50:00 -04:00
# Constants ===========================================================
LEVEL_RANK = { ' nothing ' : 0 , ' viewer ' : 1 , ' administrator ' : 2 , ' manager ' : 3 }
2026-05-31 22:01:59 -04:00
VALIDATION_FLAGS = {
' VALIDATION_IPV4_FORMAT ' : 1 << 0 ,
' VALIDATION_IPV6_FORMAT ' : 1 << 1 ,
' VALIDATION_SUBNET ' : 1 << 2 ,
' VALIDATION_ADDRESS ' : 1 << 3 ,
' VALIDATION_MAC ' : 1 << 4 ,
' VALIDATION_URL ' : 1 << 5 ,
' VALIDATION_PORT ' : 1 << 6 ,
' VALIDATION_DASH_NAME ' : 1 << 7 ,
' VALIDATION_NETWORK_NAME ' : 1 << 8 ,
' VALIDATION_DOMAIN_NAME ' : 1 << 9 ,
' VALIDATION_TIME24H ' : 1 << 10 ,
' VALIDATION_RANGE_INT ' : 1 << 11 ,
2026-06-06 00:51:30 -04:00
' VALIDATION_IPV4_CIDR ' : 1 << 12 ,
' VALIDATION_IPV4_CIDRFLEX ' : 1 << 13 ,
' VALIDATION_UNRESTRICTED ' : 1 << 14 ,
' VALIDATION_IP_OR_DOMAIN_NAME ' : 1 << 15 ,
2026-05-31 00:22:39 -04:00
}
2026-06-05 22:54:12 -04:00
def _restricted_vlan_subnets ( ) :
""" Return list of ' subnet/prefix ' strings for all restricted VLANs. """
vlans = load_config ( ) . get ( ' vlans ' , [ ] )
result = [ ]
for v in vlans :
if v . get ( ' restricted_vlan ' ) and v . get ( ' subnet ' ) and v . get ( ' subnet_mask ' ) is not None :
result . append ( f " { v [ ' subnet ' ] } / { v [ ' subnet_mask ' ] } " )
return result
2026-06-02 12:49:39 -04:00
# File / shell helpers ================================================
def load_json ( path ) :
try :
with open ( path ) as f :
return json . load ( f )
except Exception as ex :
print ( f ' [factory] ERROR loading { path } : { ex } ' , file = sys . stderr )
return { }
def load_ddns ( ) :
return load_config ( ) . get ( ' ddns ' , { } )
def load_accounts ( ) :
return load_json ( ACCOUNTS_FILE )
def run ( cmd ) :
try :
r = subprocess . run ( cmd , shell = True , capture_output = True , text = True , timeout = 5 )
return r . stdout . strip ( )
except Exception :
return ' '
def load_css ( ) :
try :
with open ( CSS_FILE ) as f :
return f . read ( )
except Exception :
return ' '
def load_icon ( name ) :
try :
with open ( f ' { WWW_DIR } /icons/ { name } .svg ' ) as f :
return f . read ( ) . strip ( )
except Exception :
return ' '
def inline_js ( page_name = None ) :
big_validate_js = build_big_validate ( )
try :
with open ( COMMON_JS_FILE ) as f :
app_js = f . read ( )
except Exception :
app_js = ' '
page_js = ' '
if page_name :
page_js_path = os . path . join ( PAGES_DIR , page_name , ' page.js ' )
try :
with open ( page_js_path ) as f :
page_js = f . read ( )
except Exception :
pass
return big_validate_js + ' \n ' + app_js + ( ' \n ' + page_js if page_js else ' ' )
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 ) :
2026-06-01 22:44:32 -04:00
""" Substitute % TOKEN % placeholders. Values are NOT auto-escaped. Callers
2026-05-28 22:50:00 -04:00
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 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
result = 0
2026-05-31 22:01:59 -04:00
for token in s . split ( ' | ' ) :
2026-05-31 00:22:39 -04:00
token = token . strip ( )
2026-05-31 22:01:59 -04:00
val = VALIDATION_FLAGS . get ( token )
2026-05-31 00:22:39 -04:00
if val is None :
print ( f ' [factory] WARNING: unknown validation token " { token } " in " { s } " ' , file = sys . stderr )
continue
result | = val
return result
def build_big_validate ( ) :
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 ' ;
}
2026-05-31 22:01:59 -04:00
function _checkDomain ( s ) {
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 ( ) ;
2026-05-31 00:22:39 -04:00
}
function _checkLine ( s ) {
var anyPartial = false , firstMsg = ' ' ;
2026-05-31 22:01:59 -04:00
function _acc ( r ) { if ( r . ok ) return r ; if ( r . partial ) anyPartial = true ; else if ( ! firstMsg ) firstMsg = r . msg ; return null ; }
var t ;
2026-06-01 09:45:55 -04:00
var fmtMask = validation & 3 ;
if ( fmtMask ) {
var fmtPassed = false ;
if ( fmtMask & 1 ) { t = _acc ( function ( ) { var rv = _ipv4 ( s ) ; if ( rv == = ' ok ' ) return _ok ( ) ; if ( rv == = ' partial ' | | rv == = ' empty ' ) return _par ( ' ' ) ; if ( rv == = ' badchar ' ) return _err ( ' Invalid character ' ) ; if ( rv == = ' badrange ' ) return _err ( ' Octet out of range ' ) ; return _err ( ' Invalid format ' ) ; } ( ) ) ; if ( t ) fmtPassed = true ; }
if ( ! fmtPassed & & ( fmtMask & 2 ) ) { t = _acc ( function ( ) { var rv = _ipv6 ( s ) ; if ( rv == = ' ok ' ) return _ok ( ) ; if ( rv == = ' partial ' | | rv == = ' empty ' ) return _par ( ' ' ) ; if ( rv == = ' badchar ' ) return _err ( ' Invalid character ' ) ; return _err ( ' Invalid format ' ) ; } ( ) ) ; if ( t ) fmtPassed = true ; }
if ( ! fmtPassed ) return anyPartial ? _par ( ' ' ) : _err ( firstMsg | | ' Invalid format ' ) ;
if ( ! ( validation & ~ 3 ) ) return _ok ( ) ;
anyPartial = false ; firstMsg = ' ' ;
}
if ( validation & 4 ) { t = _acc ( function ( ) { if ( ! arg1 ) return _par ( ' ' ) ; var prefix = parseInt ( arg1 , 10 ) ; if ( isNaN ( prefix ) | | prefix < 1 | | prefix > 30 ) return _err ( ' Prefix must be 1-30 ' ) ; if ( ! fmtMask ) { var rv = _ipv4 ( s ) ; if ( rv != = ' ok ' ) return ( rv == = ' partial ' | | rv == = ' empty ' ) ? _par ( ' ' ) : ( rv == = ' 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 ( t ) return t ; }
if ( validation & 8 ) { t = _acc ( function ( ) { if ( ! fmtMask ) { var rv = _ipv4 ( s ) ; if ( rv != = ' ok ' ) return ( rv == = ' partial ' | | rv == = ' empty ' ) ? _par ( ' ' ) : ( rv == = ' 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 ( ) ; } ( ) ) ; if ( t ) return t ; }
2026-05-31 22:01:59 -04:00
if ( validation & 16 ) { t = _acc ( function ( ) { 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 ( t ) return t ; }
if ( validation & 32 ) { t = _acc ( function ( ) { 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(t)return t;}
if ( validation & 64 ) { t = _acc ( function ( ) { 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 ( t ) return t ; }
if ( validation & 128 ) { t = _acc ( function ( ) { 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 ( t ) return t ; }
if ( validation & 256 ) { t = _acc ( function ( ) { 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 ( t ) return t ; }
if ( validation & 512 ) { t = _acc ( _checkDomain ( s ) ) ; if ( t ) return t ; }
if ( validation & 1024 ) { t = _acc ( function ( ) { 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 ( t ) return t ; }
if ( validation & 2048 ) { t = _acc ( function ( ) { 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 ( t ) return t ; }
2026-06-06 00:51:30 -04:00
if ( validation & 4096 ) { t = _acc ( function ( ) { if ( ! s ) return _par ( ' ' ) ; var slash = s . indexOf ( ' / ' ) ; if ( slash == = - 1 ) { var rv = _ipv4 ( s ) ; return ( rv == = ' ok ' | | rv == = ' partial ' | | rv == = ' empty ' ) ? _par ( ' ' ) : ( rv == = ' badchar ' ? _err ( ' Invalid character ' ) : rv == = ' badrange ' ? _err ( ' Octet out of range ' ) : _err ( ' Invalid format ' ) ) ; } var rv = _ipv4 ( s . slice ( 0 , slash ) ) ; if ( rv != = ' ok ' ) return rv == = ' badchar ' ? _err ( ' Invalid character ' ) : rv == = ' 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 ) ; if ( n < 0 | | n > 32 ) return _err ( ' Prefix must be 0-32 ' ) ; var ip = s . slice ( 0 , slash ) . split ( ' . ' ) . map ( Number ) ; var ipN = ( ( ip [ 0 ] << 24 ) | ( ip [ 1 ] << 16 ) | ( ip [ 2 ] << 8 ) | ip [ 3 ] ) >> > 0 ; var mB = n == = 0 ? 0 : ( ( 0xFFFFFFFF << ( 32 - n ) ) >> > 0 ) ; return ( ( ipN & ( ~ mB >> > 0 ) ) != = 0 ) ? _err ( ' Host bits must be zero ' ) : _ok ( ) ; } ( ) ) ; if ( t ) return t ; }
if ( validation & 8192 ) { t = _acc ( function ( ) { if ( ! s ) return _par ( ' ' ) ; var slash = s . indexOf ( ' / ' ) ; if ( slash == = - 1 ) { var rv = _ipv4 ( s ) ; if ( rv == = ' ok ' ) { var lo = parseInt ( s . split ( ' . ' ) [ 3 ] , 10 ) ; return lo == = 0 ? _par ( ' ' ) : _ok ( ) ; } return ( rv == = ' partial ' | | rv == = ' empty ' ) ? _par ( ' ' ) : ( rv == = ' badchar ' ? _err ( ' Invalid character ' ) : rv == = ' badrange ' ? _err ( ' Octet out of range ' ) : _err ( ' Invalid format ' ) ) ; } var rv = _ipv4 ( s . slice ( 0 , slash ) ) ; if ( rv != = ' ok ' ) return rv == = ' badchar ' ? _err ( ' Invalid character ' ) : rv == = ' 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 ) ; if ( n < 0 | | n > 32 ) return _err ( ' Prefix must be 0-32 ' ) ; var ip = s . slice ( 0 , slash ) . split ( ' . ' ) . map ( Number ) ; var ipN = ( ( ip [ 0 ] << 24 ) | ( ip [ 1 ] << 16 ) | ( ip [ 2 ] << 8 ) | ip [ 3 ] ) >> > 0 ; var mB = n == = 0 ? 0 : ( ( 0xFFFFFFFF << ( 32 - n ) ) >> > 0 ) ; return ( ( ipN & ( ~ mB >> > 0 ) ) != = 0 ) ? _err ( ' Host bits must be zero ' ) : _ok ( ) ; } ( ) ) ; if ( t ) return t ; }
2026-06-05 22:54:12 -04:00
if ( validation & 16384 ) { t = _acc ( function ( ) { if ( ! s ) return _par ( ' ' ) ; var rv = _ipv4 ( s ) ; if ( rv != = ' ok ' ) return _par ( ' ' ) ; if ( ! collisions | | ! collisions . length ) return _ok ( ) ; var ip = s . split ( ' . ' ) . map ( Number ) ; var ipN = ( ( ip [ 0 ] << 24 ) | ( ip [ 1 ] << 16 ) | ( ip [ 2 ] << 8 ) | ip [ 3 ] ) >> > 0 ; for ( var i = 0 ; i < collisions . length ; i + + ) { var sp = String ( collisions [ i ] ) . split ( ' / ' ) ; if ( sp . length != = 2 ) continue ; var np = sp [ 0 ] . split ( ' . ' ) . map ( Number ) ; if ( np . length != = 4 ) continue ; var netN = ( ( np [ 0 ] << 24 ) | ( np [ 1 ] << 16 ) | ( np [ 2 ] << 8 ) | np [ 3 ] ) >> > 0 ; var pfx = parseInt ( sp [ 1 ] , 10 ) ; var mB = pfx == = 0 ? 0 : ( ( 0xFFFFFFFF << ( 32 - pfx ) ) >> > 0 ) ; if ( ( ipN & mB ) == = ( netN & mB ) ) return _err ( ' IP is on a restricted VLAN ' ) ; } return _ok ( ) ; } ( ) ) ; if ( t ) return t ; }
2026-06-06 00:51:30 -04:00
if ( validation & 32768 ) { t = _acc ( function ( ) { if ( ! s ) return _par ( ' ' ) ; if ( / ^ [ 0 - 9. ] + $ / . test ( s ) ) { var rv = _ipv4 ( s ) ; return rv == = ' ok ' ? _ok ( ) : ( rv == = ' partial ' | | rv == = ' 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 _checkDomain ( s . slice ( 0 , s . lastIndexOf ( ' : ' ) ) ) ; } return _checkDomain ( s ) ; } ( ) ) ; if ( t ) return t ; }
2026-05-31 00:22:39 -04:00
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 ; }
2026-05-31 00:44:20 -04:00
if ( collisions & & collisions . some ( function ( c ) { return String ( c ) == = l ; } ) ) return _err ( ' Already in use ' ) ;
2026-05-31 00:22:39 -04:00
if ( dedup ) { if ( seen [ l ] ) return _err ( ' Duplicate entry ' ) ; seen [ l ] = true ; }
}
if ( ! hasContent ) return _par ( ' ' ) ;
if ( hasPartial ) return _par ( ' ' ) ;
return _ok ( ) ; """
2026-05-31 22:01:59 -04:00
return f ' function bigValidate(value,validation,collisions,dedup,arg1,arg2) {{ { body } \n }} '
2026-05-31 00:22:39 -04:00
2026-06-02 12:49:39 -04:00
def table_token_key ( spec ) :
return ' TABLE_ ' + re . sub ( r ' [^A-Z0-9] ' , ' _ ' , spec . upper ( ) )
def iter_table_items ( items ) :
for item in items :
2026-06-03 00:58:42 -04:00
if not isinstance ( item , dict ) :
continue
2026-06-02 12:49:39 -04:00
if item . get ( ' type ' ) == ' table ' :
yield item
2026-06-03 00:58:42 -04:00
sub_items = item . get ( ' items ' )
if not isinstance ( sub_items , list ) :
sub_items = [ ]
for sub in ( sub_items , ( item . get ( ' toolbar ' ) or { } ) . get ( ' items ' ) or [ ] ) :
2026-06-02 12:49:39 -04:00
yield from iter_table_items ( sub )
2026-05-28 22:50:00 -04:00
# 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-06-02 12:49:39 -04:00
def flatten_json ( val , prefix ) :
2026-05-31 18:24:04 -04:00
""" Recursively flatten a parsed JSON value into [(path, leaf_str)] pairs. """
if isinstance ( val , dict ) :
out = [ ]
for k , v in val . items ( ) :
2026-06-02 12:49:39 -04:00
out . extend ( flatten_json ( v , f ' { prefix } . { k } ' ) )
2026-05-31 18:24:04 -04:00
return out
if isinstance ( val , list ) :
out = [ ]
for i , v in enumerate ( val ) :
2026-06-02 12:49:39 -04:00
out . extend ( flatten_json ( v , f ' { prefix } [ { i } ] ' ) )
2026-05-31 18:24:04 -04:00
return out
if val is None :
return [ ( prefix , None ) ]
if isinstance ( val , bool ) :
return [ ( prefix , ' true ' if val else ' false ' ) ]
return [ ( prefix , str ( val ) ) ]
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 :
2026-05-31 18:24:04 -04:00
field = c [ ' field ' ]
before_text = c [ ' before ' ]
after_text = c [ ' after ' ]
vtype = c . get ( ' value_type ' , ' str ' )
if vtype == ' json ' :
try :
bval = json . loads ( before_text ) if before_text is not None else None
aval = json . loads ( after_text ) if after_text is not None else None
if isinstance ( bval , ( dict , list ) ) or isinstance ( aval , ( dict , list ) ) :
2026-06-02 12:49:39 -04:00
bflat = dict ( flatten_json ( bval , field ) ) if isinstance ( bval , ( dict , list ) ) else { }
aflat = dict ( flatten_json ( aval , field ) ) if isinstance ( aval , ( dict , list ) ) else { }
2026-05-31 18:24:04 -04:00
if bflat or aflat :
seen = set ( )
for k in list ( aflat ) + list ( bflat ) :
if k in seen :
continue
seen . add ( k )
bv = bflat . get ( k )
av = aflat . get ( k )
rows + = (
' <tr> '
f ' <td class= " snap-expand-field " > { e ( k ) } </td> '
f ' <td class= " snap-expand-val " > { e ( bv ) if bv is not None else " <em>(none)</em> " } </td> '
f ' <td class= " snap-expand-val " > { e ( av ) if av is not None else " <em>(none)</em> " } </td> '
' </tr> '
)
continue
except Exception :
pass
bval = before_text if before_text is not None else ' '
aval = after_text if after_text is not None else ' '
2026-05-30 14:57:33 -04:00
rows + = (
' <tr> '
2026-05-31 18:24:04 -04:00
f ' <td class= " snap-expand-field " > { e ( field ) } </td> '
2026-05-30 14:57:33 -04:00
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 ) )
2026-05-31 22:29:05 -04:00
if spec . get ( ' optional ' ) :
gate_vars . append ( f ' (! { vn } || ! { vn } .value.trim() || { vn } ._valid) ' )
else :
gate_vars . append ( f ' { vn } && { vn } ._valid ' )
2026-05-28 22:50:00 -04:00
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 '
2026-06-05 12:34:38 -04:00
elif input_type == ' checkbox_group ' :
try :
result [ name ] = json . loads ( value ) if value else [ ]
except Exception :
result [ name ] = [ ]
2026-05-28 22:50:00 -04:00
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 ) :
2026-06-05 21:29:42 -04:00
label = e ( apply_tokens ( item . get ( ' label ' , ' ' ) , tokens ) )
2026-05-28 22:50:00 -04:00
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 ' :
2026-06-05 21:40:42 -04:00
checked = ' checked ' if value . lower ( ) in ( ' true ' , ' 1 ' , ' yes ' ) else ' '
disabled_raw = apply_tokens ( str ( item . get ( ' disabled ' , ' ' ) ) , tokens )
disabled = ' disabled ' if disabled_raw and disabled_raw not in ( ' false ' , ' 0 ' ) else ' '
2026-05-28 22:50:00 -04:00
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 " > '
2026-06-05 21:40:42 -04:00
f ' <input type= " checkbox " name= " { name } " { checked } { disabled } class= " form-checkbox " /> '
2026-05-28 22:50:00 -04:00
f ' <span class= " form-checkbox-label " > { e ( cb_label ) } </span> '
f ' </label> { hint_html } </div> '
)
return (
' <div class= " form-group " > '
' <label class= " form-label " > '
2026-06-05 21:40:42 -04:00
f ' <input type= " checkbox " name= " { name } " { checked } { disabled } class= " form-checkbox " /> { label } '
2026-05-28 22:50:00 -04:00
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 (
2026-06-05 21:40:42 -04:00
f ' <option value= " { e ( o [ " value " ] ) } " { " selected " if o [ " value " ] == current else " " } { " disabled " if o . get ( " disabled " ) else " " } > { e ( o [ " label " ] ) } </option> '
2026-05-28 22:50:00 -04:00
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 22:29:05 -04:00
extra_attrs = ' ' . join ( f ' { e ( ak ) } = " { e ( apply_tokens ( str ( av ) , tokens ) ) } " ' for ak , av in item . get ( ' attrs ' , { } ) . items ( ) )
optional_attr = ' data-optional= " 1 " ' if item . get ( ' optional ' ) else ' '
2026-05-31 22:01:59 -04:00
existing_ids = apply_tokens ( item . get ( ' existing_ids ' , ' ' ) , tokens )
existing_attr = f ' data-existing-ids= " { e ( existing_ids ) } " ' if existing_ids else ' '
2026-06-05 22:54:12 -04:00
if not existing_ids and ( _vmask & VALIDATION_FLAGS . get ( ' VALIDATION_UNRESTRICTED ' , 0 ) ) :
_rsubnets = _restricted_vlan_subnets ( )
if _rsubnets :
existing_attr = f ' data-existing-ids= " { e ( json . dumps ( _rsubnets ) ) } " '
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 ) } " '
2026-05-31 22:29:05 -04:00
f ' placeholder= " { placeholder } " class= " form-input " { readonly } { validate_attr } { depends_attr } { extra_attrs } { optional_attr } { existing_attr } /> '
2026-05-28 22:50:00 -04:00
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 ) } " '
2026-05-31 22:29:05 -04:00
f ' placeholder= " { placeholder } " class= " form-input " { readonly } { validate_attr } { depends_attr } { extra_attrs } { optional_attr } { existing_attr } /> '
2026-05-28 22:50:00 -04:00
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 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 ' :
2026-06-01 01:06:16 -04:00
opts = render_options or { }
prefer_short = opts . get ( ' prefer_short ' , False )
2026-05-28 22:50:00 -04:00
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 ' '
2026-06-01 01:06:16 -04:00
if prefer_short :
return f ' <span class= " tag " data-tooltip= " { e ( tooltip ) } " > { e ( short ) } </span> '
2026-05-28 22:50:00 -04:00
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 ======================================================
2026-06-02 12:49:39 -04:00
def build_table ( item , tokens , rows , inherited_req = None ) :
2026-05-28 22:50:00 -04:00
level = client_level ( )
columns = item . get ( ' columns ' , [ ] )
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> '
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-row-index= " { idx } " data-row= " { row_json } " '
f ' data-target= " { target } " > { 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> '
return (
f ' { toolbar_html } '
' <div class= " table-wrapper " > '
' <table class= " data-table " > '
f ' <thead><tr> { thead } </tr></thead> '
f ' <tbody> { tbody } </tbody> '
2026-06-03 00:10:52 -04:00
f ' </table></div> '
2026-05-28 22:50:00 -04:00
)
# 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 ' :
2026-06-01 00:09:36 -04:00
inner = build_items ( item . get ( ' items ' , [ ] ) , tokens , req )
cols = item . get ( ' cols ' , 2 )
style_attr = f ' style= " { e ( item [ " style " ] ) } " ' if item . get ( ' style ' ) else ' '
return f ' <div class= " form-row- { cols } " { style_attr } > { inner } </div> '
2026-05-28 22:50:00 -04:00
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 ' :
2026-06-02 12:49:39 -04:00
ds = item . get ( ' datasource ' , ' ' )
key = table_token_key ( ds )
if key in tokens :
return Markup ( tokens [ key ] )
return build_table ( item , tokens , [ ] , req )
2026-05-28 22:50:00 -04:00
if t == ' raw_html ' :
return Markup ( apply_tokens ( item . get ( ' html ' , ' ' ) , tokens ) )
return ' '
2026-06-02 12:49:39 -04:00
# Layout renderer =====================================================
def render_layout ( view_id , content_html , tokens , page_name = None ) :
css = load_css ( )
level = client_level ( )
has_pending_alert = not _apply_changes_immediately ( ) and bool ( get_dashboard_pending ( ) )
titlebar_html = f ' <div class= " titlebar " ><span class= " titlebar-brand " > { WEB_APP_DISPLAY_NAME } </span></div> '
navbar_html = build_navbar ( view_id , level , tokens , pending_alert = has_pending_alert )
footer_html = f ' <footer class= " footer " > { WEB_APP_DISPLAY_NAME } </footer> '
page_hash = config_hash ( )
lan_iface = e ( tokens . get ( ' GENERAL_LAN_INTERFACE ' , ' ' ) )
vpn_count = tokens . get ( ' VPN_VLAN_COUNT ' , ' 0 ' )
current_user = session . get ( ' email_address ' , ' ' )
pending = get_pending_entries ( )
my_uuid = next ( ( u for u , t , c , usr in pending if usr == current_user and c != ' fix problems ' ) , None )
secs = _seconds_until_next_run ( )
locked = _is_locked ( )
lock_mtime = _lock_mtime ( )
other_bars = ' '
seen_other_users = set ( )
for o_uuid , o_ts , o_cmd , o_user in pending :
if o_user == current_user :
continue
if o_user in seen_other_users :
continue
seen_other_users . add ( o_user )
display_user = ' Another user ' if o_user in ( ' unknown ' , ' ' ) else e ( o_user )
if locked and lock_mtime and o_ts < lock_mtime :
text = f ' { display_user } \' s changes are being applied now... '
cls = ' info-bar-warning info-bar-running '
else :
timing = _format_timing ( secs )
text = (
f ' { display_user } has pending changes which will be applied { timing } . '
if timing else
f ' { display_user } has pending changes. The processing service is not running. '
)
cls = ' info-bar-warning '
other_bars + = f ' <div class= " info-bar { cls } " data-apply-uuid= " { e ( o_uuid ) } " data-apply-user= " { e ( o_user ) } " ><span> { text } </span></div> \n '
problem_bars = ' '
if level > = LEVEL_RANK [ ' viewer ' ] :
try :
st = json . load ( open ( HEALTH_FILE ) )
problems = [ ]
for section in ( ' configurations ' , ' logs ' ) :
for item in st . get ( section , [ ] ) :
if item . get ( ' status ' ) == ' problem ' :
problems . append ( e ( item . get ( ' detail ' , item . get ( ' name ' , ' ' ) ) ) )
for item in st . get ( ' services ' , [ ] ) :
if item . get ( ' status ' ) == ' problem ' :
name = item . get ( ' name ' , ' ' )
utype = ' timer ' if name . endswith ( ' .timer ' ) else ' service ' if name . endswith ( ' .service ' ) else ' unit '
exp_parts , act_parts = [ ] , [ ]
if not item . get ( ' active_ok ' ) :
exp_parts . append ( item . get ( ' expected_active ' , ' active ' ) )
act_parts . append ( item . get ( ' active ' , ' unknown ' ) )
if not item . get ( ' enabled_ok ' ) :
exp_parts . append ( item . get ( ' expected_enabled ' , ' enabled ' ) )
act_parts . append ( item . get ( ' enabled ' , ' unknown ' ) )
problems . append ( e (
f " The { utype } ` { name } ` is expected to be "
f " { ' and ' . join ( exp_parts ) } but is { ' and ' . join ( act_parts ) } . "
) )
has_problems = bool ( problems )
fix_suffix = ' '
fix_uuid = None
if has_problems :
if level < LEVEL_RANK [ ' administrator ' ] :
fix_suffix = ' Please contact an administrator. '
else :
fix_uuid , fix_ts = _find_cmd_in_queues ( ' fix problems ' )
if _apply_changes_immediately ( ) :
if _is_locked ( ) :
mtime = _lock_mtime ( )
fix_suffix = (
' Fix is being applied now... '
if fix_ts and mtime and fix_ts < mtime
else ' Fix will be applied on the next run. '
)
else :
timing = _format_timing ( _seconds_until_next_run ( ) )
fix_suffix = (
f ' Fix will be applied { timing } . '
if timing else
' Fix pending. The processing service is not running. '
)
else :
fix_suffix = (
' Fix pending. Click <strong>Apply Now</strong> below to fix. '
if view_id == ' actions ' else
' Fix pending. Visit the <strong>Actions</strong> page ASAP to apply fix. '
)
if problems :
problems_list = (
' <ul style= " margin:0.25em 0;padding-left:1.25em " > '
+ ' ' . join ( f ' <li> { d } </li> ' for d in problems )
+ ' </ul> '
)
uuid_attr = (
f ' data-health-uuid= " { e ( fix_uuid ) } " '
if fix_uuid and _entry_ts_from_queue ( fix_uuid ) is not None else ' '
)
fix_html = (
f ' <div style= " margin-top:0.5em " { uuid_attr } > { fix_suffix } </div> '
if fix_suffix else ' '
)
content = (
' <div style= " width:100 % " > '
' <div style= " font-weight:600;margin-bottom:0.25em " >Health check - problems found:</div> '
+ problems_list + fix_html
+ ' </div> '
)
problem_bars + = f ' <div class= " info-bar info-bar-danger " > { content } </div> \n '
except Exception :
pass
pending_bar = ' '
if has_pending_alert and not problem_bars and view_id != ' actions ' :
pending_bar = (
' <div class= " info-bar info-bar-warning " > '
' <span>You have actions pending. Please visit the <strong>Actions</strong> page.</span> '
' </div> \n '
)
return (
' <!DOCTYPE html> \n <html lang= " en " > \n <head> \n '
' <meta charset= " UTF-8 " /> \n '
' <meta name= " viewport " content= " width=device-width, initial-scale=1.0 " /> \n '
f ' <title> { WEB_APP_DISPLAY_NAME } </title> \n '
f ' <style> { css } </style> \n '
' </head> \n <body> \n '
f ' { titlebar_html } \n '
f ' { navbar_html } \n '
f ' <main class= " main-content " > \n { pending_bar } { problem_bars } { other_bars } { content_html } \n </main> \n '
f ' { footer_html } \n '
f ' <script>var CONFIG_HASH= " { page_hash } " ;var LAN_IFACE= " { lan_iface } " ;var VPN_VLAN_COUNT= { vpn_count } ;var APPLY_UUID= { json . dumps ( my_uuid ) } ;</script> \n '
f ' <script> { inline_js ( page_name ) } </script> \n '
' </body> \n </html> '
)
def build_navbar ( active_view , level , tokens , pending_alert = False ) :
navbar_data = load_json ( NAVBAR_FILE )
left , right = [ ] , [ ]
for item in navbar_data . get ( ' items ' , [ ] ) :
req = item . get ( ' client_requirement ' )
align = item . get ( ' align ' , ' left ' )
if not passes ( req , level ) :
continue
frag = build_nav_item ( item , active_view , level , in_dropdown = False , inherited_req = req , pending_alert = pending_alert )
( right if align == ' right ' else left ) . append ( frag )
return (
' <nav class= " nav-bar " > '
f ' <div class= " nav-left " > { " " . join ( left ) } </div> '
f ' <div class= " nav-right " > { " " . join ( right ) } </div> '
' </nav> '
)
def build_nav_item ( item , active_view , level , in_dropdown = False , inherited_req = None , pending_alert = False ) :
req = item . get ( ' client_requirement ' , inherited_req )
t = item . get ( ' type ' , ' ' )
if t in ( ' nav_item ' , ' nav_action ' ) :
label = e ( item . get ( ' label ' , ' ' ) )
map_to = item . get ( ' map_to ' , ' ' )
action = item . get ( ' action ' , ' ' )
is_active = ' active ' if map_to and map_to == active_view else ' '
pending = ' nav-item-pending ' if pending_alert and map_to == ' actions ' else ' '
cls = f ' dropdown-item { is_active } ' if in_dropdown else f ' nav-item { is_active } { pending } '
if action :
return (
f ' <form method= " post " action= " /action/ { e ( action ) } " class= " form-inline " > '
f ' <button type= " submit " class= " { cls } " > { label } </button></form> '
)
if map_to :
return f ' <a href= " / { e ( map_to ) } " class= " { cls } " > { label } </a> '
return f ' <span class= " { cls } " > { label } </span> '
if t == ' nav_menu ' :
raw_label = item . get ( ' label ' , ' ' )
if raw_label == ' % MENU_LABEL % ' :
2026-06-02 12:58:16 -04:00
raw_label = ' Network '
2026-06-02 12:49:39 -04:00
label = e ( raw_label )
children = ' '
for child in item . get ( ' items ' , [ ] ) :
child_req = child . get ( ' client_requirement ' , req )
if not passes ( child_req , level ) :
continue
children + = build_nav_item ( child , active_view , level , in_dropdown = True , inherited_req = req , pending_alert = pending_alert )
if not children :
return ' '
return (
' <div class= " nav-menu " > '
f ' <button class= " nav-item nav-menu-trigger " aria-haspopup= " true " > { label } </button> '
f ' <div class= " nav-dropdown " > { children } </div> '
' </div> '
)
return ' '