Skip to content

Commit 6ebfdca

Browse files
committed
servers add/edit/remove
1 parent 22c0005 commit 6ebfdca

5 files changed

Lines changed: 498 additions & 21 deletions

File tree

ser2tcp/html/app.js

Lines changed: 238 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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'};
186193
const PARITIES = ['NONE', 'EVEN', 'ODD', 'MARK', 'SPACE'];
187194
const 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(/^edit\/(.+)$/);
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(/^port(\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

12011239
function 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">&#9998;</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">&#9998;</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 ---
13061530
function 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);

ser2tcp/html/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<nav>
1515
<button class="active" data-tab="ports">Ports</button>
1616
<button data-tab="users">Users</button>
17+
<button data-tab="settings">Settings</button>
1718
</nav>
1819
</div>
1920
<div class="topbar-right">
@@ -72,6 +73,12 @@ <h3>Add user</h3>
7273
<div id="user-error" class="error hidden"></div>
7374
</div>
7475
</div>
76+
<div id="tab-settings" class="hidden">
77+
<div id="http-servers"></div>
78+
<div class="toolbar">
79+
<button class="btn-primary" id="add-http-btn">+ Add HTTP Server</button>
80+
</div>
81+
</div>
7582
</div>
7683
</div>
7784

0 commit comments

Comments
 (0)