@@ -129,8 +129,12 @@ function showApp(initialData) {
129129 document . querySelector ( '.topbar' ) . classList . remove ( 'hidden' ) ;
130130 updateUserInfo ( ) ;
131131 const hash = location . hash . slice ( 1 ) ;
132- const tab = [ 'ports' , 'users' ] . includes ( hash ) ? hash : 'ports' ;
133- switchTab ( tab , tab === 'ports' ? initialData : null ) ;
132+ if ( [ 'users' , 'settings' ] . includes ( hash ) ) {
133+ switchTab ( hash ) ;
134+ } else {
135+ // ports tab - check for edit/new
136+ switchTab ( 'ports' , initialData , hash ) ;
137+ }
134138}
135139
136140// --- Login/Logout ---
@@ -162,16 +166,19 @@ function doLogout() {
162166}
163167
164168// --- Tabs ---
165- function switchTab ( tab , data ) {
169+ function switchTab ( tab , data , hash ) {
166170 document . querySelectorAll ( 'nav button[data-tab]' ) . forEach (
167171 b => b . classList . toggle ( 'active' , b . dataset . tab === tab ) ) ;
168172 document . querySelectorAll ( '[id^="tab-"]' ) . forEach (
169173 t => t . classList . toggle ( 'hidden' , t . id !== 'tab-' + tab ) ) ;
170- if ( tab === 'ports' ) loadPorts ( data ) ;
174+ if ( tab === 'ports' ) loadPorts ( data , hash ) ;
171175 else if ( tab === 'users' ) loadUsers ( ) ;
172- const newPath = tab === 'ports' ? location . pathname : '#' + tab ;
173- if ( location . hash !== ( tab === 'ports' ? '' : '#' + tab ) )
174- history . pushState ( null , '' , newPath ) ;
176+ else if ( tab === 'settings' ) loadSettings ( ) ;
177+ if ( ! hash ) {
178+ const newPath = tab === 'ports' ? location . pathname : '#' + tab ;
179+ if ( location . hash !== ( tab === 'ports' ? '' : '#' + tab ) )
180+ history . pushState ( null , '' , newPath ) ;
181+ }
175182}
176183
177184// --- Ports ---
@@ -186,7 +193,7 @@ const BYTESIZES = {8: 'EIGHTBITS', 7: 'SEVENBITS', 6: 'SIXBITS', 5: 'FIVEBITS'};
186193const PARITIES = [ 'NONE' , 'EVEN' , 'ODD' , 'MARK' , 'SPACE' ] ;
187194const STOPBITS = { '1' : 'ONE' , '1.5' : 'ONE_POINT_FIVE' , '2' : 'TWO' } ;
188195
189- function loadPorts ( statusData ) {
196+ function loadPorts ( statusData , hash ) {
190197 const root = $ ( 'ports-content' ) ;
191198 const render = ( status , detected ) => {
192199 detectedPorts = detected || [ ] ;
@@ -204,6 +211,25 @@ function loadPorts(statusData) {
204211 }
205212 status . ports . forEach ( ( p , i ) => root . appendChild ( renderPortCard ( p , i ) ) ) ;
206213 renderDetectedSection ( ) ;
214+ // Open editor if hash indicates
215+ if ( hash ) {
216+ const editMatch = hash . match ( / ^ e d i t \/ ( .+ ) $ / ) ;
217+ if ( editMatch ) {
218+ const name = decodeURIComponent ( editMatch [ 1 ] ) ;
219+ // Find port by name or fallback to index if name is "portN"
220+ let idx = status . ports . findIndex ( p => p . name === name ) ;
221+ if ( idx < 0 ) {
222+ const indexMatch = name . match ( / ^ p o r t ( \d + ) $ / ) ;
223+ if ( indexMatch ) idx = parseInt ( indexMatch [ 1 ] ) ;
224+ }
225+ if ( idx >= 0 && idx < status . ports . length ) {
226+ const config = buildConfigFromStatus ( status . ports [ idx ] ) ;
227+ showPortEditor ( idx , config , true ) ;
228+ }
229+ } else if ( hash === 'new' ) {
230+ addPort ( ) ;
231+ }
232+ }
207233 } ;
208234 if ( statusData ) {
209235 api ( 'GET' , '/api/detect' ) . then ( detected => {
@@ -514,7 +540,7 @@ function buildConfigFromStatus(port) {
514540 return config ;
515541}
516542
517- function showPortEditor ( index , config ) {
543+ function showPortEditor ( index , config , skipHistory ) {
518544 const root = $ ( 'ports-content' ) ;
519545 // Find existing card or append
520546 let container ;
@@ -528,6 +554,12 @@ function showPortEditor(index, config) {
528554 container . className = 'port-edit' ;
529555 container . dataset . portIndex = index !== null ? index : 'new' ;
530556 container . replaceChildren ( ) ;
557+ // Update URL with name
558+ if ( ! skipHistory ) {
559+ const name = config . name || ( index !== null ? 'port' + index : null ) ;
560+ const hash = name ? '#edit/' + encodeURIComponent ( name ) : '#new' ;
561+ history . pushState ( null , '' , hash ) ;
562+ }
531563
532564 const title = index !== null ? 'Edit Port ' + index : 'New Port' ;
533565 container . appendChild ( el ( 'h3' , title ) ) ;
@@ -702,7 +734,10 @@ function showPortEditor(index, config) {
702734 saveBtn . onclick = ( ) => savePort ( index ) ;
703735 actions . appendChild ( saveBtn ) ;
704736 const cancelBtn = el ( 'button' , 'Cancel' , 'btn-secondary' ) ;
705- cancelBtn . onclick = ( ) => loadPorts ( ) ;
737+ cancelBtn . onclick = ( ) => {
738+ history . pushState ( null , '' , location . pathname ) ;
739+ loadPorts ( ) ;
740+ } ;
706741 actions . appendChild ( cancelBtn ) ;
707742 if ( index !== null ) {
708743 const delBtn = el ( 'button' , 'Delete' , 'btn-danger' ) ;
@@ -1187,7 +1222,10 @@ function savePort(index) {
11871222 const config = collectConfig ( ) ;
11881223 const method = index !== null ? 'PUT' : 'POST' ;
11891224 const path = index !== null ? '/api/ports/' + index : '/api/ports' ;
1190- api ( method , path , config ) . then ( ( ) => loadPorts ( ) ) . catch ( e => {
1225+ api ( method , path , config ) . then ( ( ) => {
1226+ history . pushState ( null , '' , location . pathname ) ;
1227+ loadPorts ( ) ;
1228+ } ) . catch ( e => {
11911229 if ( e !== 'unauthorized' ) alert ( e ) ;
11921230 } ) ;
11931231}
@@ -1200,7 +1238,10 @@ function disconnectClient(portIdx, srvIdx, conIdx) {
12001238
12011239function deletePort ( index ) {
12021240 if ( ! confirm ( 'Delete port ' + index + '?' ) ) return ;
1203- api ( 'DELETE' , '/api/ports/' + index ) . then ( ( ) => loadPorts ( ) ) . catch ( e => {
1241+ api ( 'DELETE' , '/api/ports/' + index ) . then ( ( ) => {
1242+ history . pushState ( null , '' , location . pathname ) ;
1243+ loadPorts ( ) ;
1244+ } ) . catch ( e => {
12041245 if ( e !== 'unauthorized' ) alert ( e ) ;
12051246 } ) ;
12061247}
@@ -1302,6 +1343,189 @@ async function addUser() {
13021343 } ) ;
13031344}
13041345
1346+ // --- Settings ---
1347+ let currentSettings = null ;
1348+
1349+ function loadSettings ( ) {
1350+ api ( 'GET' , '/api/settings' ) . then ( data => {
1351+ currentSettings = data ;
1352+ renderSettings ( ) ;
1353+ } ) . catch ( e => {
1354+ if ( e !== 'unauthorized' ) console . error ( 'Failed to load settings:' , e ) ;
1355+ } ) ;
1356+ }
1357+
1358+ function renderSettings ( ) {
1359+ const container = $ ( 'http-servers' ) ;
1360+ container . innerHTML = '' ;
1361+
1362+ // Session timeout card
1363+ const timeoutCard = document . createElement ( 'div' ) ;
1364+ timeoutCard . className = 'section' ;
1365+ timeoutCard . dataset . httpIndex = 'session' ;
1366+ const timeout = currentSettings . session_timeout ;
1367+ timeoutCard . innerHTML = `
1368+ <button class="btn-edit" title="Edit">✎</button>
1369+ <h2>Session</h2>
1370+ <dl>
1371+ <dt>Timeout</dt>
1372+ <dd>${ timeout != null ? timeout + 's' : 'default' } </dd>
1373+ </dl>
1374+ ` ;
1375+ timeoutCard . querySelector ( '.btn-edit' ) . addEventListener ( 'click' , ( ) => showSessionEditor ( ) ) ;
1376+ container . appendChild ( timeoutCard ) ;
1377+
1378+ // HTTP server cards
1379+ const servers = currentSettings . http || [ ] ;
1380+ servers . forEach ( ( srv , i ) => {
1381+ container . appendChild ( renderHttpCard ( srv , i ) ) ;
1382+ } ) ;
1383+ }
1384+
1385+ function renderHttpCard ( srv , index ) {
1386+ const card = document . createElement ( 'div' ) ;
1387+ card . className = 'section' ;
1388+ card . dataset . httpIndex = index ;
1389+ const ssl = srv . ssl ? ' (SSL)' : '' ;
1390+ const title = srv . name || `${ srv . address } :${ srv . port } ${ ssl } ` ;
1391+ card . innerHTML = `
1392+ <button class="btn-edit" title="Edit">✎</button>
1393+ <h2>${ title } </h2>
1394+ <dl>
1395+ <dt>Address</dt><dd>${ srv . address || '0.0.0.0' } :${ srv . port } ${ ssl } </dd>
1396+ ${ srv . ssl ? `<dt>Cert</dt><dd>${ srv . ssl . certfile || '-' } </dd>` : '' }
1397+ </dl>
1398+ ` ;
1399+ card . querySelector ( '.btn-edit' ) . addEventListener ( 'click' , ( ) => showHttpEditor ( index , srv ) ) ;
1400+ return card ;
1401+ }
1402+
1403+ function showSessionEditor ( ) {
1404+ const container = $ ( 'http-servers' ) ;
1405+ const existing = container . querySelector ( '[data-http-index="session"]' ) ;
1406+ const card = document . createElement ( 'div' ) ;
1407+ card . className = 'section http-edit' ;
1408+ card . dataset . httpIndex = 'session' ;
1409+ const timeout = currentSettings . session_timeout ;
1410+ card . innerHTML = `
1411+ <h3>Session</h3>
1412+ <div class="field-row">
1413+ <label>Timeout (seconds):</label>
1414+ <input type="number" id="edit-session-timeout" min="0" placeholder="3600" value="${ timeout || '' } ">
1415+ </div>
1416+ <div class="edit-buttons">
1417+ <button type="button" class="btn-primary" id="save-session-btn">Save</button>
1418+ <button type="button" id="cancel-session-btn">Cancel</button>
1419+ </div>
1420+ ` ;
1421+ existing . replaceWith ( card ) ;
1422+ card . querySelector ( '#save-session-btn' ) . addEventListener ( 'click' , ( ) => {
1423+ const val = $ ( 'edit-session-timeout' ) . value ;
1424+ const t = val . trim ( ) === '' ? null : parseInt ( val ) ;
1425+ if ( val . trim ( ) !== '' && ( isNaN ( t ) || t < 0 ) ) {
1426+ alert ( 'Invalid timeout value' ) ;
1427+ return ;
1428+ }
1429+ api ( 'PUT' , '/api/settings' , { session_timeout : t } ) . then ( ( ) => loadSettings ( ) )
1430+ . catch ( e => { if ( e !== 'unauthorized' ) alert ( e ) ; } ) ;
1431+ } ) ;
1432+ card . querySelector ( '#cancel-session-btn' ) . addEventListener ( 'click' , ( ) => loadSettings ( ) ) ;
1433+ }
1434+
1435+ function showHttpEditor ( index , srv ) {
1436+ const container = $ ( 'http-servers' ) ;
1437+ const isNew = index === null ;
1438+ let card ;
1439+ if ( isNew ) {
1440+ card = document . createElement ( 'div' ) ;
1441+ container . appendChild ( card ) ;
1442+ } else {
1443+ card = container . querySelector ( '[data-http-index="' + index + '"]' ) ;
1444+ }
1445+ card . className = 'section http-edit' ;
1446+ card . dataset . httpIndex = isNew ? 'new' : index ;
1447+ const title = isNew ? 'New HTTP Server' : 'Edit HTTP Server' ;
1448+ card . innerHTML = `
1449+ <h3>${ title } </h3>
1450+ <div class="field-row">
1451+ <label>Name:</label>
1452+ <input type="text" class="http-name" value="${ srv . name || '' } " placeholder="optional">
1453+ </div>
1454+ <div class="field-row">
1455+ <label>Address:</label>
1456+ <input type="text" class="http-address" value="${ srv . address || '0.0.0.0' } " placeholder="0.0.0.0">
1457+ </div>
1458+ <div class="field-row">
1459+ <label>Port:</label>
1460+ <input type="number" class="http-port" value="${ srv . port || 8080 } " min="1" max="65535">
1461+ </div>
1462+ <div class="field-row">
1463+ <label><input type="checkbox" class="http-ssl" ${ srv . ssl ? 'checked' : '' } > SSL</label>
1464+ </div>
1465+ <div class="ssl-fields ${ srv . ssl ? '' : 'hidden' } ">
1466+ <div class="field-row">
1467+ <label>Certificate:</label>
1468+ <input type="text" class="http-certfile" value="${ srv . ssl ?. certfile || '' } " placeholder="/path/to/cert.pem">
1469+ </div>
1470+ <div class="field-row">
1471+ <label>Key:</label>
1472+ <input type="text" class="http-keyfile" value="${ srv . ssl ?. keyfile || '' } " placeholder="/path/to/key.pem">
1473+ </div>
1474+ </div>
1475+ <div class="edit-buttons">
1476+ <button type="button" class="btn-primary http-save-btn">Save</button>
1477+ ${ ! isNew ? '<button type="button" class="btn-danger http-delete-btn">Delete</button>' : '' }
1478+ <button type="button" class="http-cancel-btn">Cancel</button>
1479+ </div>
1480+ ` ;
1481+ card . querySelector ( '.http-ssl' ) . addEventListener ( 'change' , e => {
1482+ card . querySelector ( '.ssl-fields' ) . classList . toggle ( 'hidden' , ! e . target . checked ) ;
1483+ } ) ;
1484+ card . querySelector ( '.http-save-btn' ) . addEventListener ( 'click' , ( ) => saveHttpServer ( isNew ? null : index , card ) ) ;
1485+ card . querySelector ( '.http-cancel-btn' ) . addEventListener ( 'click' , ( ) => loadSettings ( ) ) ;
1486+ if ( ! isNew ) {
1487+ card . querySelector ( '.http-delete-btn' ) . addEventListener ( 'click' , ( ) => deleteHttpServer ( index ) ) ;
1488+ }
1489+ }
1490+
1491+ function saveHttpServer ( index , card ) {
1492+ const data = {
1493+ address : card . querySelector ( '.http-address' ) . value . trim ( ) || '0.0.0.0' ,
1494+ port : parseInt ( card . querySelector ( '.http-port' ) . value ) || 8080 ,
1495+ } ;
1496+ const name = card . querySelector ( '.http-name' ) . value . trim ( ) ;
1497+ if ( name ) data . name = name ;
1498+ if ( card . querySelector ( '.http-ssl' ) . checked ) {
1499+ const certfile = card . querySelector ( '.http-certfile' ) . value . trim ( ) ;
1500+ const keyfile = card . querySelector ( '.http-keyfile' ) . value . trim ( ) ;
1501+ if ( ! certfile || ! keyfile ) {
1502+ alert ( 'SSL requires certificate and key file paths' ) ;
1503+ return ;
1504+ }
1505+ data . ssl = { certfile, keyfile} ;
1506+ }
1507+ const method = index === null ? 'POST' : 'PUT' ;
1508+ const path = index === null ? '/api/settings/http' : '/api/settings/http/' + index ;
1509+ api ( method , path , data ) . then ( ( ) => {
1510+ loadSettings ( ) ;
1511+ } ) . catch ( e => {
1512+ if ( e !== 'unauthorized' ) alert ( e ) ;
1513+ } ) ;
1514+ }
1515+
1516+ function deleteHttpServer ( index ) {
1517+ if ( ! confirm ( 'Delete this HTTP server?' ) ) return ;
1518+ api ( 'DELETE' , '/api/settings/http/' + index ) . then ( ( ) => {
1519+ loadSettings ( ) ;
1520+ } ) . catch ( e => {
1521+ if ( e !== 'unauthorized' ) alert ( e ) ;
1522+ } ) ;
1523+ }
1524+
1525+ function addHttpServer ( ) {
1526+ showHttpEditor ( null , { address : '0.0.0.0' , port : 8080 } ) ;
1527+ }
1528+
13051529// --- Init ---
13061530function init ( ) {
13071531 initTheme ( ) ;
@@ -1311,13 +1535,14 @@ function init() {
13111535 $ ( 'logout-btn' ) . addEventListener ( 'click' , doLogout ) ;
13121536 $ ( 'add-user-btn' ) . addEventListener ( 'click' , addUser ) ;
13131537 $ ( 'add-port-btn' ) . addEventListener ( 'click' , addPort ) ;
1538+ $ ( 'add-http-btn' ) . addEventListener ( 'click' , addHttpServer ) ;
13141539
13151540 document . querySelectorAll ( 'nav button[data-tab]' ) . forEach ( btn => {
13161541 btn . addEventListener ( 'click' , ( ) => switchTab ( btn . dataset . tab ) ) ;
13171542 } ) ;
13181543 window . addEventListener ( 'hashchange' , ( ) => {
13191544 const tab = location . hash . slice ( 1 ) ;
1320- if ( [ 'ports' , 'users' ] . includes ( tab ) ) switchTab ( tab ) ;
1545+ if ( [ 'ports' , 'users' , 'settings' ] . includes ( tab ) ) switchTab ( tab ) ;
13211546 } ) ;
13221547
13231548 api ( 'GET' , '/api/status' ) . then ( showApp ) . catch ( showLogin ) ;
0 commit comments