2026-06-19 14:30:41 +03:00
<!DOCTYPE html>
< html lang = "ru" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > {% block title %}mita panel{% endblock %}< / title >
< style >
: root { --bg : #0d0f14 ; --surface : #141720 ; --surface2 : #1c2030 ; --border : #252a3a ;
--accent : #4f8fff ; --accent2 : #7b5fff ; --ok : #3ecf8e ; --warn : #f5a623 ;
--danger : #ff5f6d ; --text : #e2e8f4 ; --muted : #6b7594 ;
--mono : 'JetBrains Mono' , 'Fira Mono' , monospace ; --sans : 'Inter' , system-ui , sans-serif ;
--radius : 10 px ; --radius-lg : 16 px }
* , * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 }
html { height : 100 % ; font-size : 15 px }
body { font-family : var ( - - sans ) ; background : var ( - - bg ) ; color : var ( - - text ) ; min-height : 100 % ; display : flex }
. sidebar { width : 220 px ; flex-shrink : 0 ; background : var ( - - surface ) ; border-right : 1 px solid var ( - - border ) ;
display : flex ; flex-direction : column ; padding : 28 px 0 20 px ; position : fixed ; top : 0 ; left : 0 ; height : 100 vh ; z-index : 100 }
. sidebar-logo { font-size : 1.2 rem ; font-weight : 700 ; color : var ( - - text ) ; padding : 0 24 px 28 px ; display : flex ; align-items : center ; gap : 10 px }
. logo-badge { background : linear-gradient ( 135 deg , var ( - - accent ) , var ( - - accent2 ) ) ; color : #fff ;
font-size : .75 rem ; font-weight : 700 ; padding : 4 px 8 px ; border-radius : 6 px ; letter-spacing : .5 px }
. nav-item { display : flex ; align-items : center ; gap : 12 px ; padding : 10 px 24 px ; color : var ( - - muted ) ;
text-decoration : none ; font-size : .875 rem ; font-weight : 500 ;
border-top : none ; border-right : none ; border-bottom : none ; border-left : 3 px solid transparent ;
background : none ; width : 100 % ; text-align : left ; transition : .15 s ; cursor : pointer }
. nav-item : hover { color : var ( - - text ) ; background : var ( - - surface2 ) }
. nav-item . active { color : var ( - - accent ) ; border-left-color : var ( - - accent ) ; background : rgba ( 79 , 143 , 255 , .08 ) }
. nav-icon { width : 18 px ; height : 18 px ; flex-shrink : 0 }
. sidebar-footer { margin-top : auto ; padding : 16 px 24 px ; border-top : 1 px solid var ( - - border ) }
. sidebar-footer a { color : var ( - - muted ) ; font-size : .8 rem ; text-decoration : none }
. sidebar-footer a : hover { color : var ( - - danger ) }
. main { margin-left : 220 px ; flex : 1 ; padding : 36 px 40 px ; min-height : 100 vh }
. page { display : none } . page . active { display : block }
h1 { font-size : 1.5 rem ; font-weight : 700 ; margin-bottom : 4 px ; letter-spacing : -.3 px }
. subtitle { color : var ( - - muted ) ; font-size : .875 rem ; margin-bottom : 28 px }
. card { background : var ( - - surface ) ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - lg ) ; padding : 24 px ; margin-bottom : 20 px }
. card-title { font-size : .75 rem ; font-weight : 600 ; text-transform : uppercase ; letter-spacing : .8 px ; color : var ( - - muted ) ; margin-bottom : 16 px }
. grid-4 { display : grid ; grid-template-columns : repeat ( 4 , 1 fr ) ; gap : 16 px ; margin-bottom : 20 px }
. grid-2 { display : grid ; grid-template-columns : repeat ( 2 , 1 fr ) ; gap : 16 px ; margin-bottom : 20 px }
. stat-card { background : var ( - - surface ) ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - lg ) ; padding : 20 px }
. stat-label { font-size : .75 rem ; color : var ( - - muted ) ; font-weight : 500 ; margin-bottom : 8 px ; text-transform : uppercase ; letter-spacing : .6 px }
. stat-value { font-size : 1.75 rem ; font-weight : 700 ; font-family : var ( - - mono ) ; color : var ( - - text ) ; line-height : 1 }
. stat-value . accent { color : var ( - - accent ) } . stat-value . ok { color : var ( - - ok ) }
. stat-sub { font-size : .75 rem ; color : var ( - - muted ) ; margin-top : 6 px }
. ip-badge { font-family : var ( - - mono ) ; font-size : .8 rem ; background : var ( - - surface2 ) ; border : 1 px solid var ( - - border ) ; padding : 3 px 8 px ; border-radius : 6 px ; color : var ( - - accent ) }
. btn { display : inline-flex ; align-items : center ; gap : 8 px ; padding : 9 px 18 px ; border-radius : var ( - - radius ) ;
font-size : .875 rem ; font-weight : 600 ; border : 1 px solid transparent ; cursor : pointer ; transition : .15 s ; text-decoration : none ; white-space : nowrap ; font-family : var ( - - sans ) }
. btn-primary { background : var ( - - accent ) ; color : #fff ; border-color : var ( - - accent ) } . btn-primary : hover { background : #3d7aff }
. btn-secondary { background : var ( - - surface2 ) ; color : var ( - - text ) ; border-color : var ( - - border ) } . btn-secondary : hover { border-color : var ( - - accent ) ; color : var ( - - accent ) }
. btn-danger { background : transparent ; color : var ( - - danger ) ; border-color : var ( - - danger ) } . btn-danger : hover { background : var ( - - danger ) ; color : #fff }
. btn-ok { background : var ( - - ok ) ; color : #0d0f14 ; border-color : var ( - - ok ) } . btn-ok : hover { background : #2fb87a }
. btn-warn { background : var ( - - warn ) ; color : #0d0f14 ; border-color : var ( - - warn ) }
. btn-sm { padding : 5 px 12 px ; font-size : .8 rem } . btn-group { display : flex ; gap : 10 px ; flex-wrap : wrap ; margin-top : 16 px }
. form-row { display : flex ; gap : 12 px ; align-items : flex-end ; flex-wrap : wrap ; margin-bottom : 16 px }
. form-group { display : flex ; flex-direction : column ; gap : 6 px ; flex : 1 ; min-width : 160 px }
label { font-size : .8 rem ; font-weight : 500 ; color : var ( - - muted ) }
input , select { background : var ( - - surface2 ) ; border : 1 px solid var ( - - border ) ; color : var ( - - text ) ;
padding : 9 px 12 px ; border-radius : var ( - - radius ) ; font-size : .875 rem ; font-family : var ( - - sans ) ; width : 100 % ; outline : none ; transition : .15 s }
input : focus , select : focus { border-color : var ( - - accent ) ; box-shadow : 0 0 0 3 px rgba ( 79 , 143 , 255 , .15 ) }
select option { background : var ( - - surface2 ) }
. table-wrap { overflow-x : auto }
table { width : 100 % ; border-collapse : collapse ; font-size : .875 rem }
th { text-align : left ; color : var ( - - muted ) ; font-size : .75 rem ; font-weight : 600 ; text-transform : uppercase ;
letter-spacing : .6 px ; padding : 10 px 12 px ; border-bottom : 1 px solid var ( - - border ) }
td { padding : 10 px 12 px ; border-bottom : 1 px solid rgba ( 37 , 42 , 58 , .6 ) ; vertical-align : middle }
tr : last-child td { border-bottom : none }
tr : hover td { background : rgba ( 255 , 255 , 255 , .02 ) }
. badge { display : inline-block ; padding : 2 px 8 px ; border-radius : 20 px ; font-size : .75 rem ; font-weight : 600 }
. badge-ok { background : rgba ( 62 , 207 , 142 , .15 ) ; color : var ( - - ok ) }
. badge-warn { background : rgba ( 245 , 166 , 35 , .15 ) ; color : var ( - - warn ) }
. badge-off { background : var ( - - surface2 ) ; color : var ( - - muted ) }
. toggle { position : relative ; display : inline-block ; width : 38 px ; height : 22 px }
. toggle input { opacity : 0 ; width : 0 ; height : 0 ; position : absolute }
. toggle-slider { position : absolute ; inset : 0 ; background : var ( - - border ) ; border-radius : 22 px ; cursor : pointer ; transition : .2 s }
. toggle-slider :: before { content : '' ; position : absolute ; width : 16 px ; height : 16 px ; left : 3 px ; top : 3 px ; background : #fff ; border-radius : 50 % ; transition : .2 s }
. toggle input : checked + . toggle-slider { background : var ( - - accent ) }
. toggle input : checked + . toggle-slider :: before { transform : translateX ( 16 px ) }
. modal-overlay { display : none ; position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , .75 ) ; z-index : 200 ; align-items : center ; justify-content : center ; padding : 20 px }
. modal-overlay . open { display : flex }
. modal { background : var ( - - surface ) ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - lg ) ;
padding : 28 px ; max-width : 700 px ; width : 100 % ; max-height : 90 vh ; overflow-y : auto ; box-shadow : 0 20 px 60 px rgba ( 0 , 0 , 0 , .6 ) }
. modal-title { font-size : 1.1 rem ; font-weight : 700 ; margin-bottom : 20 px ; display : flex ; justify-content : space-between ; align-items : center }
. modal-close { background : none ; border : none ; color : var ( - - muted ) ; font-size : 1.3 rem ; cursor : pointer ; padding : 4 px ; line-height : 1 }
. modal-close : hover { color : var ( - - danger ) }
. code-block { background : var ( - - bg ) ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius ) ;
padding : 14 px ; font-family : var ( - - mono ) ; font-size : .78 rem ; color : var ( - - ok ) ;
overflow-x : auto ; white-space : pre-wrap ; word-break : break-all ; max-height : 260 px ; overflow-y : auto }
. accordion-item { border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius ) ; margin-bottom : 8 px ; overflow : hidden }
. accordion-header { padding : 12 px 16 px ; cursor : pointer ; display : flex ; justify-content : space-between ; align-items : center ;
background : var ( - - surface2 ) ; font-weight : 600 ; font-size : .875 rem ; user-select : none }
. accordion-header : hover { background : var ( - - border ) }
. accordion-body { display : none ; padding : 16 px ; background : var ( - - surface ) }
. accordion-body . open { display : block }
. accordion-arrow { transition : .2 s ; color : var ( - - muted ) ; display : inline-block }
. accordion-header . aopen . accordion-arrow { transform : rotate ( 180 deg ) }
. alert { padding : 12 px 16 px ; border-radius : var ( - - radius ) ; font-size : .875 rem ; margin-bottom : 16 px }
. alert-ok { background : rgba ( 62 , 207 , 142 , .12 ) ; border : 1 px solid rgba ( 62 , 207 , 142 , .3 ) ; color : var ( - - ok ) }
. alert-err { background : rgba ( 255 , 95 , 109 , .12 ) ; border : 1 px solid rgba ( 255 , 95 , 109 , .3 ) ; color : var ( - - danger ) }
. alert-warn { background : rgba ( 245 , 166 , 35 , .12 ) ; border : 1 px solid rgba ( 245 , 166 , 35 , .3 ) ; color : var ( - - warn ) }
. spinner { display : inline-block ; width : 14 px ; height : 14 px ; border : 2 px solid rgba ( 255 , 255 , 255 , .2 ) ;
border-top-color : #fff ; border-radius : 50 % ; animation : spin .6 s linear infinite ; vertical-align : middle }
@ keyframes spin { to { transform : rotate ( 360 deg ) } }
. divider { height : 1 px ; background : var ( - - border ) ; margin : 24 px 0 }
. row-actions { display : flex ; gap : 8 px ; align-items : center }
. section-head { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : 20 px }
< / style >
< / head >
< body >
< nav class = "sidebar" >
< div class = "sidebar-logo" >
< div class = "logo-badge" > MITA< / div > Panel
< / div >
< button class = "nav-item active" onclick = "showPage('dashboard',this)" >
< svg class = "nav-icon" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" / > < / svg >
Дашборд
< / button >
< button class = "nav-item" onclick = "showPage('users',this)" >
< svg class = "nav-icon" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" / > < / svg >
Пользователи
< / button >
< button class = "nav-item" onclick = "showPage('ssl',this)" >
< svg class = "nav-icon" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" / > < / svg >
SSL
< / button >
< button class = "nav-item" onclick = "showPage('security',this)" >
< svg class = "nav-icon" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" / > < / svg >
Безопасность
< / button >
2026-06-26 23:37:05 +03:00
< button class = "nav-item" onclick = "showPage('bot',this)" >
< svg class = "nav-icon" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" / > < / svg >
Telegram бот
< / button >
2026-06-19 14:30:41 +03:00
< div class = "sidebar-footer" > < a href = "{{ base }}/logout" > Выйти< / a > < / div >
< / nav >
< main class = "main" > {% block content %}{% endblock %}< / main >
< div class = "modal-overlay" id = "modal" >
< div class = "modal" >
< div class = "modal-title" >
< span id = "modal-title-text" > < / span >
< button class = "modal-close" onclick = "closeModal()" > ✕ < / button >
< / div >
< div id = "modal-body" > < / div >
< / div >
< / div >
< div id = "toast" style = "position:fixed;bottom:24px;right:24px;z-index:999;display:none;
background:var(--surface2);border:1px solid var(--border);color:var(--ok);
padding:10px 18px;border-radius:8px;font-size:.85rem;transition:.2s" > < / div >
2026-06-20 18:00:00 +03:00
< script > var QRCode ; ! function ( ) { function a ( a ) { this . mode = c . MODE _8BIT _BYTE , this . data = a , this . parsedData = [ ] ; for ( var b = [ ] , d = 0 , e = this . data . length ; e > d ; d ++ ) { var f = this . data . charCodeAt ( d ) ; f > 65536 ? ( b [ 0 ] = 240 | ( 1835008 & f ) >>> 18 , b [ 1 ] = 128 | ( 258048 & f ) >>> 12 , b [ 2 ] = 128 | ( 4032 & f ) >>> 6 , b [ 3 ] = 128 | 63 & f ) : f > 2048 ? ( b [ 0 ] = 224 | ( 61440 & f ) >>> 12 , b [ 1 ] = 128 | ( 4032 & f ) >>> 6 , b [ 2 ] = 128 | 63 & f ) : f > 128 ? ( b [ 0 ] = 192 | ( 1984 & f ) >>> 6 , b [ 1 ] = 128 | 63 & f ) : b [ 0 ] = f , this . parsedData = this . parsedData . concat ( b ) } this . parsedData . length != this . data . length && ( this . parsedData . unshift ( 191 ) , this . parsedData . unshift ( 187 ) , this . parsedData . unshift ( 239 ) ) } function b ( a , b ) { this . typeNumber = a , this . errorCorrectLevel = b , this . modules = null , this . moduleCount = 0 , this . dataCache = null , this . dataList = [ ] } function i ( a , b ) { if ( void 0 == a . length ) throw new Error ( a . length + "/" + b ) ; for ( var c = 0 ; c < a . length && 0 == a [ c ] ; ) c ++ ; this . num = new Array ( a . length - c + b ) ; for ( var d = 0 ; d < a . length - c ; d ++ ) this . num [ d ] = a [ d + c ] } function j ( a , b ) { this . totalCount = a , this . dataCount = b } function k ( ) { this . buffer = [ ] , this . length = 0 } function m ( ) { return "undefined" != typeof CanvasRenderingContext2D } function n ( ) { var a = ! 1 , b = navigator . userAgent ; return /android/i . test ( b ) && ( a = ! 0 , aMat = b . toString ( ) . match ( /android ([0-9]\.[0-9])/i ) , aMat && aMat [ 1 ] && ( a = parseFloat ( aMat [ 1 ] ) ) ) , a } function r ( a , b ) { for ( var c = 1 , e = s ( a ) , f = 0 , g = l . length ; g >= f ; f ++ ) { var h = 0 ; switch ( b ) { case d . L : h = l [ f ] [ 0 ] ; break ; case d . M : h = l [ f ] [ 1 ] ; break ; case d . Q : h = l [ f ] [ 2 ] ; break ; case d . H : h = l [ f ] [ 3 ] } if ( h >= e ) break ; c ++ } if ( c > l . length ) throw new Error ( "Too long data" ) ; return c } function s ( a ) { var b = encodeURI ( a ) . toString ( ) . replace ( /\%[0-9a-fA-F]{2}/g , "a" ) ; return b . length + ( b . length != a ? 3 : 0 ) } a . prototype = { getLength : function ( ) { return this . parsedData . length } , write : function ( a ) { for ( var b = 0 , c = this . parsedData . length ; c > b ; b ++ ) a . put ( this . parsedData [ b ] , 8 ) } } , b . prototype = { addData : function ( b ) { var c = new a ( b ) ; this . dataList . push ( c ) , this . dataCache = null } , isDark : function ( a , b ) { if ( 0 > a || this . moduleCount <= a || 0 > b || this . moduleCount <= b ) throw new Error ( a + "," + b ) ; return this . modules [ a ] [ b ] } , getModuleCount : function ( ) { return this . moduleCount } , make : function ( ) { this . makeImpl ( ! 1 , this . getBestMaskPattern ( ) ) } , makeImpl : function ( a , c ) { this . moduleCount = 4 * this . typeNumber + 17 , this . modules = new Array ( this . moduleCount ) ; for ( var d = 0 ; d < this . moduleCount ; d ++ ) { this . modules [ d ] = new Array ( this . moduleCount ) ; for ( var e = 0 ; e < this . moduleCount ; e ++ ) this . modules [ d ] [ e ] = null } this . setupPositionProbePattern ( 0 , 0 ) , this . setupPositionProbePattern ( this . moduleCount - 7 , 0 ) , this . setupPositionProbePattern ( 0 , this . moduleCount - 7 ) , this . setupPositionAdjustPattern ( ) , this . setupTimingPattern ( ) , this . setupTypeInfo ( a , c ) , this . typeNumber >= 7 && this . setupTypeNumber ( a ) , null == this . dataCache && ( this . dataCache = b . createData ( this . typeNumber , this . errorCorrectLevel , this . dataList ) ) , this . mapData ( this . dataCache , c ) } , setupPositionProbePattern : function ( a , b ) { for ( var c = - 1 ; 7 >= c ; c ++ ) if ( ! ( - 1 >= a + c || this . moduleCount <= a + c ) ) for ( var d = - 1 ; 7 >= d ; d ++ ) - 1 >= b + d || this . moduleCount <= b + d || ( this . modules [ a + c ] [ b + d ] = c >= 0 && 6 >= c && ( 0 == d || 6 == d ) || d >= 0 && 6 >= d && ( 0 == c || 6 == c ) || c >= 2 && 4 >= c && d >= 2 && 4 >= d ? ! 0 : ! 1 ) } , getBestMaskPattern : function ( ) { for ( var a = 0 , b = 0 , c = 0 ; 8 > c ; c ++ ) { this . makeImpl ( ! 0 , c ) ; var d = f . getLostPoint ( this ) ; ( 0 == c || a > d ) && ( a = d , b = c ) } return b } , createMovieClip : function ( a , b , c ) { var d = a . createEmptyMovieClip ( b , c ) , e = 1 ; this . make ( ) ; for ( var f = 0 ; f < this . modules . length ; f ++ ) for ( var g = f * e , h = 0 ; h < this . modules [ f ] . length ; h ++ ) { var i = h * e , j = this . modules [ f ] [ h ] ; j && ( d . beginFill ( 0 , 100 ) , d . moveTo ( i , g ) , d . lineTo ( i + e , g ) , d . lineTo ( i + e , g + e ) , d . lineTo ( i , g + e ) , d . endFill ( ) ) } return d } , setupTimingPattern : function ( ) { for ( var a = 8 ; a < this . moduleCount - 8 ; a ++ ) null == this . modules [ a ] [ 6 ] && ( this . modules [ a ] [ 6 ] = 0 == a % 2 ) ; for ( var b = 8 ; b < this . moduleCount - 8 ; b ++ ) null == this . modules [ 6 ] [ b ] && ( this . modules [ 6 ] [ b ] = 0 == b % 2 ) } , setupPositionAdjustPattern : function ( ) { for ( var a = f . getPatternPosition ( this . typeNumber ) , b = 0 ; b < a . length ; b ++ ) for ( var c = 0 ; c < a . length ; c ++ ) { var d = a [ b ] , e = a [ c ] ; if ( null == this . modules [ d ] [ e ] ) for ( var g = - 2 ; 2 >= g ; g ++ ) for ( var h = - 2 ; 2 >= h ; h ++ ) this . modules [ d + g ] [ e + h ] = - 2 == g || 2 == g || - 2 == h || 2 == h || 0 == g && 0 == h ? ! 0 : ! 1 } } , setupTypeNumber : function ( a ) { for ( var b = f . getBCHTypeNumber ( this . typeNumber ) , c = 0 ; 18 > c ; c ++ ) { var d = ! a && 1 == ( 1 & b >> c ) ; this . modules [ Math . floor ( c / 3 ) ] [ c % 3 + this . moduleCount - 8 - 3 ] = d } for ( var
2026-06-19 14:30:41 +03:00
< script >
const BASE = "{{ base }}" ;
function showPage ( id , btn ) {
document . querySelectorAll ( '.page' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
document . querySelectorAll ( '.nav-item' ) . forEach ( n => n . classList . remove ( 'active' ) ) ;
document . getElementById ( 'page-' + id ) . classList . add ( 'active' ) ;
if ( btn ) btn . classList . add ( 'active' ) ;
2026-06-26 23:37:05 +03:00
if ( id === 'dashboard' ) { loadDashboard ( ) ; if ( typeof startStatsPolling === 'function' ) startStatsPolling ( ) ; }
else { if ( typeof stopStatsPolling === 'function' ) stopStatsPolling ( ) ; }
2026-06-19 14:30:41 +03:00
if ( id === 'users' ) loadUsers ( ) ;
if ( id === 'ssl' ) loadSSL ( ) ;
2026-06-19 15:37:02 +03:00
if ( id === 'security' ) loadSecurityPage ( ) ;
2026-06-26 23:37:05 +03:00
if ( id === 'bot' ) loadBotPage ( ) ;
2026-06-19 14:30:41 +03:00
}
function openModal ( title , html ) {
document . getElementById ( 'modal-title-text' ) . textContent = title ;
document . getElementById ( 'modal-body' ) . innerHTML = html ;
document . getElementById ( 'modal' ) . classList . add ( 'open' ) ;
}
function closeModal ( ) { document . getElementById ( 'modal' ) . classList . remove ( 'open' ) ; }
document . getElementById ( 'modal' ) . addEventListener ( 'click' , e => { if ( e . target . id === 'modal' ) closeModal ( ) ; } ) ;
async function api ( path , method = 'GET' , body = null ) {
const opts = { method , headers : { 'Content-Type' : 'application/json' } } ;
if ( body ) opts . body = JSON . stringify ( body ) ;
const r = await fetch ( BASE + path , opts ) ;
if ( r . status === 401 ) { location . href = BASE + '/login' ; return null ; }
return r . json ( ) ;
}
function showToast ( msg , isErr = false ) {
const t = document . getElementById ( 'toast' ) ;
t . textContent = msg ; t . style . color = isErr ? 'var(--danger)' : 'var(--ok)' ;
t . style . display = 'block' ; setTimeout ( ( ) => t . style . display = 'none' , 2500 ) ;
}
2026-06-19 22:46:43 +03:00
function copyText ( text ) {
if ( navigator . clipboard && navigator . clipboard . writeText ) {
navigator . clipboard . writeText ( text ) . then ( ( ) => showToast ( 'Скопировано ✓' ) ) . catch ( ( ) => _copyFallback ( text ) ) ;
} else { _copyFallback ( text ) ; }
}
function _copyFallback ( text ) {
const ta = document . createElement ( 'textarea' ) ;
ta . value = text ; ta . style . cssText = 'position:fixed;opacity:0;top:0;left:0' ;
document . body . appendChild ( ta ) ; ta . focus ( ) ; ta . select ( ) ;
try { document . execCommand ( 'copy' ) ; showToast ( 'Скопировано ✓' ) ; }
catch ( e ) { showToast ( 'Ошибка копирования' , true ) ; }
document . body . removeChild ( ta ) ;
}
2026-06-19 14:30:41 +03:00
function toggleAccordion ( hdr ) {
hdr . classList . toggle ( 'aopen' ) ;
hdr . nextElementSibling . classList . toggle ( 'open' ) ;
}
function codeBlock ( json ) {
const txt = JSON . stringify ( json , null , 2 ) ;
return ` <div class="code-block" id="cb_ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } "> ${ escHtml ( txt ) } </div>
<button class="btn btn-secondary btn-sm" style="margin-top:8px" onclick="copyText( ${ escAttr ( txt ) } )">Копировать</button> ` ;
}
function escHtml ( s ) { return s . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) ; }
function escAttr ( s ) { return "'" + s . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) + "'" ; }
< / script >
{% block scripts %}{% endblock %}
< / body >
< / html >