Pārlūkot izejas kodu

feat(services): add Edit Port modal and live port reconfiguration in Web UI and CLI

Proxmox Router 1 nedēļu atpakaļ
vecāks
revīzija
87ec494b1a

BIN
bin/proxmox-router


+ 5 - 7
cmd/server/main.go

@@ -91,11 +91,10 @@ func main() {
 	cliTelnetServer := cli.NewServer(telnetAddr, authMgr, cliExecutor)
 	cliSSHServer := cli.NewSSHServer(sshAddr, authMgr, cliExecutor)
 
-	// Register hooks for dynamic service start/stop
+	// Register hooks for dynamic service start/stop & port changes
 	servicesMgr.RegisterHook("telnet", func(serviceName string, port int, disabled bool) error {
-		if disabled {
-			cliTelnetServer.Stop()
-		} else {
+		cliTelnetServer.Stop()
+		if !disabled {
 			cliTelnetServer.SetAddr(fmt.Sprintf(":%d", port))
 			go func() {
 				if err := cliTelnetServer.Start(); err != nil {
@@ -107,9 +106,8 @@ func main() {
 	})
 
 	servicesMgr.RegisterHook("ssh", func(serviceName string, port int, disabled bool) error {
-		if disabled {
-			cliSSHServer.Stop()
-		} else {
+		cliSSHServer.Stop()
+		if !disabled {
 			cliSSHServer.SetAddr(fmt.Sprintf(":%d", port))
 			go func() {
 				if err := cliSSHServer.Start(); err != nil {

+ 1 - 0
internal/api/api.go

@@ -422,6 +422,7 @@ func (s *Server) registerRoutes() {
 	// Management Services (/ip service)
 	s.mux.HandleFunc("GET /api/system/services", s.handleListServices)
 	s.mux.HandleFunc("POST /api/system/services/{name}", s.handleUpdateService)
+	s.mux.HandleFunc("PUT /api/system/services/{name}", s.handleUpdateService)
 	s.mux.HandleFunc("POST /api/system/services/{name}/state", s.handleSetServiceState)
 
 	// Logging (/log)

+ 78 - 2
internal/web/static/app.js

@@ -16473,17 +16473,93 @@ function renderServicesTable() {
     return `
       <tr class="hover:bg-slate-800/40 transition">
         <td class="py-3 px-4 font-bold text-white uppercase">${s.name}</td>
-        <td class="py-3 px-4 text-cyan-400 font-mono">${s.port}</td>
+        <td class="py-3 px-4">
+          <div class="flex items-center gap-2">
+            <span class="text-cyan-400 font-mono font-bold">${s.port}</span>
+            <button onclick="openEditServiceModal('${s.name}', ${s.port}, ${s.disabled})" title="Edit Port" class="text-slate-500 hover:text-cyan-400 p-1 rounded hover:bg-slate-800/80 transition">
+              <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
+            </button>
+          </div>
+        </td>
         <td class="py-3 px-4">${statusBadge}</td>
         <td class="py-3 px-4 text-slate-400 text-xs font-sans">${s.comment || '-'}</td>
         <td class="py-3 px-4 text-right">
-          ${toggleBtn}
+          <div class="flex items-center justify-end gap-2">
+            <button onclick="openEditServiceModal('${s.name}', ${s.port}, ${s.disabled})" class="px-2.5 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white border border-slate-700 text-[11px] font-medium transition inline-flex items-center gap-1">
+              <svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
+              <span>Edit Port</span>
+            </button>
+            ${toggleBtn}
+          </div>
         </td>
       </tr>
     `;
   }).join('');
 }
 
+function openEditServiceModal(serviceName, currentPort, isDisabled) {
+  document.getElementById('modal-service-name').value = serviceName;
+  document.getElementById('modal-service-disabled').value = String(isDisabled);
+  document.getElementById('modal-service-display-name').textContent = serviceName.toUpperCase();
+  document.getElementById('modal-service-port').value = currentPort;
+
+  const hintEl = document.getElementById('modal-service-port-hint');
+  const warnEl = document.getElementById('modal-service-warning');
+
+  if (serviceName === 'www') {
+    hintEl.textContent = 'Default HTTP web port: 80';
+    warnEl.classList.remove('hidden');
+  } else if (serviceName === 'telnet') {
+    hintEl.textContent = 'Default Telnet / debtik-cli console port: 8282';
+    warnEl.classList.add('hidden');
+  } else if (serviceName === 'ssh') {
+    hintEl.textContent = 'Default CLI SSH port: 2222';
+    warnEl.classList.add('hidden');
+  } else {
+    hintEl.textContent = '';
+    warnEl.classList.add('hidden');
+  }
+
+  const modal = document.getElementById('modal-service-edit');
+  modal.classList.remove('hidden');
+  setTimeout(() => document.getElementById('modal-service-port').focus(), 50);
+}
+
+function closeEditServiceModal() {
+  const modal = document.getElementById('modal-service-edit');
+  if (modal) modal.classList.add('hidden');
+}
+
+async function submitSaveServicePort(event) {
+  event.preventDefault();
+  const name = document.getElementById('modal-service-name').value;
+  const disabled = document.getElementById('modal-service-disabled').value === 'true';
+  const port = parseInt(document.getElementById('modal-service-port').value, 10);
+
+  if (isNaN(port) || port < 1 || port > 65535) {
+    showToast('Invalid Port', 'Port must be a valid number between 1 and 65535', 'error');
+    return;
+  }
+
+  try {
+    const res = await fetch(`${API_BASE}/system/services/${name}`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ port: port, disabled: disabled })
+    });
+    if (!res.ok) {
+      const errData = await res.json();
+      throw new Error(errData.message || 'Failed to update service port');
+    }
+
+    closeEditServiceModal();
+    showToast('Port Updated', `Service ${name.toUpperCase()} port changed to ${port}`, 'success');
+    await loadServices();
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
 async function toggleServiceState(serviceName, newDisabled) {
   if (serviceName === 'www' && newDisabled) {
     if (!confirm("WARNING: Disabling the Web service ('www') will turn off this Web UI and REST API. You will only be able to re-enable it via SSH (port 2222) or Telnet (port 8282) using 'ip service enable www'. Continue?")) {

+ 34 - 1
internal/web/static/index.html

@@ -11661,7 +11661,40 @@
     </div>
   </div>
 
+  <!-- ================= MODAL: EDIT SERVICE PORT ================= -->
+  <div id="modal-service-edit" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden">
+      <div class="px-5 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/50">
+        <h3 id="modal-service-title" class="text-sm font-bold text-white flex items-center gap-2">
+          <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/></svg>
+          <span>Edit Service Port</span>
+        </h3>
+        <button type="button" onclick="closeEditServiceModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <form id="form-service-edit" onsubmit="submitSaveServicePort(event)" class="p-5 space-y-4">
+        <input type="hidden" id="modal-service-name">
+        <input type="hidden" id="modal-service-disabled">
+        <div>
+          <label class="block text-xs font-semibold text-slate-300 mb-1">Service</label>
+          <div id="modal-service-display-name" class="font-mono text-xs font-bold text-cyan-400 bg-slate-950/80 border border-slate-800 rounded-lg px-3 py-2 uppercase">SSH</div>
+        </div>
+        <div>
+          <label class="block text-xs font-semibold text-slate-300 mb-1">Listening Port (1 - 65535) *</label>
+          <input type="number" id="modal-service-port" min="1" max="65535" required class="w-full bg-slate-950 border border-slate-800 rounded-lg px-3 py-2 text-xs text-white font-mono focus:outline-none focus:border-cyan-500">
+          <p id="modal-service-port-hint" class="text-[10px] text-slate-500 mt-1"></p>
+        </div>
+        <div id="modal-service-warning" class="hidden p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-300 text-xs">
+          ⚠️ <strong>Προσοχή:</strong> Η αλλαγή της πόρτας του Web UI θα αλλάξει τη διεύθυνση URL με την οποία συνδέεσαι.
+        </div>
+        <div class="pt-2 flex items-center justify-end space-x-2">
+          <button type="button" onclick="closeEditServiceModal()" class="px-3.5 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs font-medium text-slate-300 transition">Cancel</button>
+          <button type="submit" class="px-4 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-xs font-semibold text-white shadow transition">Save Port</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
   <datalist id="fw-addrlist-datalist"></datalist>
-  <script src="app.js?v=15.0"></script>
+  <script src="app.js?v=16.0"></script>
 </body>
 </html>