@@ -6,6 +6,57 @@ const el = (tag, text, cls) => {
66 return e ;
77} ;
88
9+ // Theme switcher
10+ function applyTheme ( theme ) {
11+ if ( theme === 'auto' ) {
12+ document . documentElement . removeAttribute ( 'data-theme' ) ;
13+ } else {
14+ document . documentElement . setAttribute ( 'data-theme' , theme ) ;
15+ }
16+ // Update icon visibility
17+ [ 'light' , 'dark' , 'auto' ] . forEach ( t => {
18+ const icon = $ ( 'theme-icon-' + t ) ;
19+ if ( icon ) icon . classList . toggle ( 'active' , t === theme ) ;
20+ } ) ;
21+ // Update dropdown active state
22+ const dropdown = $ ( 'theme-dropdown' ) ;
23+ if ( dropdown ) {
24+ dropdown . querySelectorAll ( 'button' ) . forEach ( btn => {
25+ btn . classList . toggle ( 'active' , btn . dataset . theme === theme ) ;
26+ } ) ;
27+ }
28+ }
29+
30+ function initTheme ( ) {
31+ const saved = localStorage . getItem ( 'ser2tcp_theme' ) || 'auto' ;
32+ const btn = $ ( 'theme-btn' ) ;
33+ const dropdown = $ ( 'theme-dropdown' ) ;
34+ if ( btn && dropdown ) {
35+ btn . onclick = e => {
36+ e . stopPropagation ( ) ;
37+ dropdown . classList . toggle ( 'open' ) ;
38+ } ;
39+ dropdown . querySelectorAll ( 'button' ) . forEach ( b => {
40+ b . onclick = ( ) => {
41+ const theme = b . dataset . theme ;
42+ localStorage . setItem ( 'ser2tcp_theme' , theme ) ;
43+ applyTheme ( theme ) ;
44+ dropdown . classList . remove ( 'open' ) ;
45+ } ;
46+ } ) ;
47+ document . addEventListener ( 'click' , ( ) => dropdown . classList . remove ( 'open' ) ) ;
48+ }
49+ applyTheme ( saved ) ;
50+ }
51+
52+ // Apply theme immediately to avoid flash
53+ ( function ( ) {
54+ const saved = localStorage . getItem ( 'ser2tcp_theme' ) ;
55+ if ( saved && saved !== 'auto' ) {
56+ document . documentElement . setAttribute ( 'data-theme' , saved ) ;
57+ }
58+ } ) ( ) ;
59+
960// Hash password with SHA-256 and random salt (same format as server)
1061async function hashPassword ( password ) {
1162 const salt = Array . from ( crypto . getRandomValues ( new Uint8Array ( 16 ) ) )
@@ -118,7 +169,9 @@ function switchTab(tab, data) {
118169 t => t . classList . toggle ( 'hidden' , t . id !== 'tab-' + tab ) ) ;
119170 if ( tab === 'ports' ) loadPorts ( data ) ;
120171 else if ( tab === 'users' ) loadUsers ( ) ;
121- if ( location . hash !== '#' + tab ) history . pushState ( null , '' , '#' + tab ) ;
172+ const newPath = tab === 'ports' ? location . pathname : '#' + tab ;
173+ if ( location . hash !== ( tab === 'ports' ? '' : '#' + tab ) )
174+ history . pushState ( null , '' , newPath ) ;
122175}
123176
124177// --- Ports ---
@@ -277,26 +330,19 @@ function renderPortCard(port, index) {
277330 const urlEl = el ( 'div' , wsUrl , 'port-config-detail' ) ;
278331 urlEl . style . cursor = 'pointer' ;
279332 urlEl . title = 'Click to copy' ;
280- urlEl . onclick = ( ) => {
333+ urlEl . onclick = e => {
334+ e . stopPropagation ( ) ;
281335 navigator . clipboard . writeText ( wsUrl ) ;
282336 urlEl . textContent = 'Copied!' ;
283337 setTimeout ( ( ) => { urlEl . textContent = wsUrl ; } , 1000 ) ;
284338 } ;
285339 li . appendChild ( urlEl ) ;
286340 if ( s . data !== false ) {
287- const termLink = document . createElement ( 'a' ) ;
288- termLink . href = '/xterm/' + s . endpoint ;
289- termLink . className = 'detect-link' ;
290- termLink . textContent = 'Terminal' ;
291- termLink . target = '_blank' ;
292341 const linksDiv = el ( 'div' , null , 'ws-links' ) ;
293- linksDiv . appendChild ( termLink ) ;
294- const rawLink = document . createElement ( 'a' ) ;
295- rawLink . href = '/raw/' + s . endpoint ;
296- rawLink . className = 'detect-link' ;
297- rawLink . textContent = 'Raw' ;
298- rawLink . target = '_blank' ;
299- linksDiv . appendChild ( rawLink ) ;
342+ linksDiv . innerHTML = '<a href="/xterm/' + s . endpoint
343+ + '" class="detect-link" target="_blank" rel="noopener">Terminal</a>'
344+ + '<a href="/raw/' + s . endpoint
345+ + '" class="detect-link" target="_blank" rel="noopener">Raw</a>' ;
300346 li . appendChild ( linksDiv ) ;
301347 }
302348 if ( s . data === false )
@@ -730,7 +776,8 @@ function renderServerBox(srv, index, total) {
730776 const box = el ( 'div' , null , 'server-box' ) ;
731777 box . dataset . serverIndex = index ;
732778
733- const removeBtn = el ( 'button' , '\u00d7' , 'btn-remove' ) ;
779+ const removeBtn = el ( 'button' , null , 'btn-remove' ) ;
780+ removeBtn . innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14"/></svg>' ;
734781 removeBtn . disabled = total <= 1 ;
735782 removeBtn . onclick = ( ) => removeServerBox ( box ) ;
736783 box . appendChild ( removeBtn ) ;
@@ -912,6 +959,29 @@ function renderServerBox(srv, index, total) {
912959 updateCtlVisibility ( ) ;
913960 box . appendChild ( ctlDiv ) ;
914961
962+ // IP filter section
963+ const ipDiv = el ( 'div' ) ;
964+ ipDiv . className = 'srv-ip-filter' ;
965+ const ipAllowRow = el ( 'div' , null , 'field-row' ) ;
966+ ipAllowRow . appendChild ( el ( 'label' , 'Allow IPs:' ) ) ;
967+ const ipAllowInput = document . createElement ( 'input' ) ;
968+ ipAllowInput . type = 'text' ;
969+ ipAllowInput . className = 'srv-allow' ;
970+ ipAllowInput . placeholder = '192.168.1.0/24, 10.0.0.5' ;
971+ ipAllowInput . value = ( srv . allow || [ ] ) . join ( ', ' ) ;
972+ ipAllowRow . appendChild ( ipAllowInput ) ;
973+ ipDiv . appendChild ( ipAllowRow ) ;
974+ const ipDenyRow = el ( 'div' , null , 'field-row' ) ;
975+ ipDenyRow . appendChild ( el ( 'label' , 'Deny IPs:' ) ) ;
976+ const ipDenyInput = document . createElement ( 'input' ) ;
977+ ipDenyInput . type = 'text' ;
978+ ipDenyInput . className = 'srv-deny' ;
979+ ipDenyInput . placeholder = '192.168.1.100' ;
980+ ipDenyInput . value = ( srv . deny || [ ] ) . join ( ', ' ) ;
981+ ipDenyRow . appendChild ( ipDenyInput ) ;
982+ ipDiv . appendChild ( ipDenyRow ) ;
983+ box . appendChild ( ipDiv ) ;
984+
915985 // Update visibility based on protocol
916986 const updateProtoFields = ( ) => {
917987 const proto = protoSel . value ;
@@ -925,6 +995,7 @@ function renderServerBox(srv, index, total) {
925995 addrLabel . textContent = isSocket ? 'Path:' : 'Address:' ;
926996 sslDiv . classList . toggle ( 'hidden' , ! isSsl ) ;
927997 ctlDiv . classList . toggle ( 'hidden' , isTelnet ) ;
998+ ipDiv . classList . toggle ( 'hidden' , isSocket ) ;
928999 // Update control description
9291000 ctlDesc . textContent = '' ;
9301001 if ( isWs ) {
@@ -1095,6 +1166,17 @@ function collectConfig() {
10951166 if ( pollMs ) ctl . poll_interval = pollMs / 1000 ;
10961167 srv . control = ctl ;
10971168 }
1169+ // IP filter
1170+ if ( proto !== 'socket' ) {
1171+ const allowStr = box . querySelector ( '.srv-allow' ) . value . trim ( ) ;
1172+ if ( allowStr ) {
1173+ srv . allow = allowStr . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( s => s ) ;
1174+ }
1175+ const denyStr = box . querySelector ( '.srv-deny' ) . value . trim ( ) ;
1176+ if ( denyStr ) {
1177+ srv . deny = denyStr . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( s => s ) ;
1178+ }
1179+ }
10981180 config . servers . push ( srv ) ;
10991181 } ) ;
11001182
@@ -1222,6 +1304,7 @@ async function addUser() {
12221304
12231305// --- Init ---
12241306function init ( ) {
1307+ initTheme ( ) ;
12251308 $ ( 'login-btn' ) . addEventListener ( 'click' , doLogin ) ;
12261309 $ ( 'login-pass' ) . addEventListener ( 'keydown' ,
12271310 e => { if ( e . key === 'Enter' ) doLogin ( ) ; } ) ;
0 commit comments