Showing 1 changed files with 92 additions and 22 deletions
+92 -22
scripts/host_manager.pl
@@ -3056,10 +3056,13 @@ sub app_html {
3056 3056
     #page-vhosts .host-tools { flex-wrap: wrap; }
3057 3057
     #page-vhosts .host-tools input { max-width: 280px; }
3058 3058
     #page-vhosts .stats { justify-content: flex-end; }
3059
-    #page-vhosts .table-wrap { overflow-x: auto; }
3060
-    #page-vhosts table { min-width: 1290px; }
3059
+    #page-vhosts .table-wrap { overflow-x: visible; }
3060
+    #page-vhosts table { min-width: 0; }
3061 3061
     #page-vhosts th, #page-vhosts td { overflow-wrap: normal; }
3062 3062
     #page-vhosts .pill.vhost { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: top; }
3063
+    .vhost-name-cell { display: grid; gap: 5px; min-width: 0; }
3064
+    .vhost-name-main { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 6px; min-width: 0; }
3065
+    .vhost-delete { min-height: 28px; padding: 3px 7px; color: var(--bad); font-size: 12px; }
3063 3066
     .vhost-host { display: grid; gap: 2px; }
3064 3067
     .vhost-pill-row { display: flex; flex-wrap: wrap; gap: 4px; }
3065 3068
     .vhost-pill-row .pill { margin: 0; }
@@ -3072,7 +3075,6 @@ sub app_html {
3072 3075
     .vhost-cert-links .linkbtn { padding: 3px 7px; font-size: 12px; }
3073 3076
     .vhost-cert-validity { font-size: 12px; }
3074 3077
     .vhost-inline-editor { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(260px, 1fr) auto; gap: 8px; padding: 10px; border-bottom: 1px solid var(--line); background: #fff; }
3075
-    .vhost-delete { color: var(--bad); }
3076 3078
     .host-inline-row td { padding: 0; background: #fff; }
3077 3079
     .host-inline-editor-shell { background: #fff; }
3078 3080
     .host-inline-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: #fafbfc; }
@@ -3212,14 +3214,12 @@ sub app_html {
3212 3214
             <table>
3213 3215
               <thead>
3214 3216
                 <tr>
3215
-                  <th style="width: 220px">Vhost</th>
3216
-                  <th style="width: 230px">Host</th>
3217
-                  <th style="width: 120px">IP</th>
3218
-                  <th style="width: 160px">Derived aliases</th>
3219
-                  <th style="width: 300px">Certificate</th>
3220
-                  <th style="width: 100px">Monitoring</th>
3221
-                  <th style="width: 80px">Status</th>
3222
-                  <th style="width: 80px">Actions</th>
3217
+                  <th style="width: 22%">Vhost</th>
3218
+                  <th style="width: 24%">Host</th>
3219
+                  <th style="width: 10%">IP</th>
3220
+                  <th style="width: 30%">Certificate</th>
3221
+                  <th style="width: 8%">Monitoring</th>
3222
+                  <th style="width: 6%">Status</th>
3223 3223
                 </tr>
3224 3224
               </thead>
3225 3225
               <tbody id="vhosts"></tbody>
@@ -3889,7 +3889,7 @@ sub app_html {
3889 3889
         ['total', vhostRows().length],
3890 3890
       ].map(([k, v]) => `<span class="stat">${escapeHtml(k)}: ${escapeHtml(String(v))}</span>`).join('');
3891 3891
       $('vhosts').innerHTML = rows.length ? rows.map(row => `<tr>
3892
-        <td><span class="pill vhost">${escapeHtml(row.vhost)}</span></td>
3892
+        <td>${renderVhostNameCell(row)}</td>
3893 3893
         <td>
3894 3894
           <div class="vhost-host">
3895 3895
             <select class="vhost-host-select" data-vhost-select="${escapeHtml(row.vhost)}" data-current-host="${escapeHtml(row.host_fqdn)}">
@@ -3898,12 +3898,10 @@ sub app_html {
3898 3898
           </div>
3899 3899
         </td>
3900 3900
         <td>${escapeHtml(row.ip)}</td>
3901
-        <td><div class="vhost-pill-row">${row.derived_aliases.map(name => `<span class="pill derived vhost">${escapeHtml(name)}</span>`).join('')}</div></td>
3902 3901
         <td>${renderVhostCertificateCell(row)}</td>
3903 3902
         <td><span class="pill">${escapeHtml(row.monitoring)}</span></td>
3904 3903
         <td>${escapeHtml(row.status)}</td>
3905
-        <td><button type="button" class="vhost-delete" data-vhost-delete="${escapeHtml(row.vhost)}">Delete</button></td>
3906
-      </tr>`).join('') : '<tr><td colspan="8" class="muted">No vhosts.</td></tr>';
3904
+      </tr>`).join('') : '<tr><td colspan="6" class="muted">No vhosts.</td></tr>';
3907 3905
       document.querySelectorAll('[data-vhost-select]').forEach(select => {
3908 3906
         select.addEventListener('change', () => {
3909 3907
           reassignVhostFromSelect(select).catch(e => {
@@ -3936,6 +3934,17 @@ sub app_html {
3936 3934
       });
3937 3935
     }
3938 3936
 
3937
+    function renderVhostNameCell(row) {
3938
+      const aliases = (row.derived_aliases || []).map(name => `<span class="pill derived vhost">${escapeHtml(name)}</span>`).join('');
3939
+      return `<div class="vhost-name-cell">
3940
+        <div class="vhost-name-main">
3941
+          <span class="pill vhost" title="${escapeHtml(row.vhost)}">${escapeHtml(row.vhost)}</span>
3942
+          <button type="button" class="vhost-delete" data-vhost-delete="${escapeHtml(row.vhost)}" title="Delete ${escapeHtml(row.vhost)}">Del</button>
3943
+        </div>
3944
+        ${aliases ? `<div class="vhost-pill-row">${aliases}</div>` : ''}
3945
+      </div>`;
3946
+    }
3947
+
3939 3948
     function renderVhostCertificateCell(row) {
3940 3949
       const cert = row.certificate || {};
3941 3950
       const certId = row.certificate_id || cert.id || cert.name || '';
@@ -3947,7 +3956,7 @@ sub app_html {
3947 3956
       return `<div class="vhost-cert">
3948 3957
         <div class="vhost-cert-main">
3949 3958
           <select class="vhost-cert-select" data-vhost-cert-select="${escapeHtml(row.vhost)}" data-current-certificate="${escapeHtml(certId)}">
3950
-            ${renderCertificateOptions(certId)}
3959
+            ${renderCertificateOptions(certId, row)}
3951 3960
           </select>
3952 3961
           <button type="button" data-vhost-cert-issue="${escapeHtml(row.vhost)}" data-current-certificate="${escapeHtml(certId)}">Issue</button>
3953 3962
         </div>
@@ -3973,19 +3982,80 @@ sub app_html {
3973 3982
         }).join('');
3974 3983
     }
3975 3984
 
3976
-    function renderCertificateOptions(selectedCertificateId) {
3977
-      const certs = (state.certificates || [])
3978
-        .slice()
3979
-        .sort((a, b) => String(a.name || a.id || '').localeCompare(String(b.name || b.id || '')));
3985
+    function renderCertificateOptions(selectedCertificateId, row) {
3986
+      const byId = new Map();
3987
+      (state.certificates || []).forEach(cert => {
3988
+        const id = certId(cert);
3989
+        if (id) byId.set(id, cert);
3990
+      });
3991
+      if (row && row.certificate) {
3992
+        const id = certId(row.certificate);
3993
+        if (id && !byId.has(id)) byId.set(id, row.certificate);
3994
+      }
3995
+      const certs = Array.from(byId.values())
3996
+        .filter(cert => certMatchesRow(cert, row) || certId(cert) === selectedCertificateId)
3997
+        .sort((a, b) => {
3998
+          const ar = certRelevance(a, row);
3999
+          const br = certRelevance(b, row);
4000
+          if (ar !== br) return ar - br;
4001
+          return String(a.name || a.id || '').localeCompare(String(b.name || b.id || ''));
4002
+        });
3980 4003
       const options = ['<option value="">no certificate</option>'].concat(certs.map(cert => {
3981
-        const id = cert.id || cert.name || '';
3982
-        const label = cert.name || cert.id || '';
4004
+        const id = certId(cert);
4005
+        const label = compactCertificateLabel(cert, row);
3983 4006
         const selected = id === selectedCertificateId ? ' selected' : '';
3984 4007
         return `<option value="${escapeHtml(id)}"${selected}>${escapeHtml(label)}</option>`;
3985 4008
       }));
3986 4009
       return options.join('');
3987 4010
     }
3988 4011
 
4012
+    function certId(cert) {
4013
+      return cert ? (cert.id || cert.name || '') : '';
4014
+    }
4015
+
4016
+    function certDnsNames(cert) {
4017
+      return (cert && Array.isArray(cert.dns_names) ? cert.dns_names : [])
4018
+        .map(name => String(name || '').toLowerCase())
4019
+        .filter(Boolean);
4020
+    }
4021
+
4022
+    function certRelevance(cert, row) {
4023
+      if (!row) return 9;
4024
+      const names = new Set(certDnsNames(cert));
4025
+      const id = String(certId(cert)).toLowerCase();
4026
+      const commonName = String(cert.common_name || '').toLowerCase();
4027
+      const vhost = String(row.vhost || '').toLowerCase();
4028
+      const host = String(row.host_fqdn || '').toLowerCase();
4029
+      const vhostShort = shortAliasForFqdn(vhost);
4030
+      const hostShort = shortAliasForFqdn(host);
4031
+      if (vhost && (names.has(vhost) || commonName === vhost || id.startsWith(vhost + '-'))) return 0;
4032
+      if (host && (names.has(host) || commonName === host || String(cert.host_fqdn || '').toLowerCase() === host || id.startsWith(host + '-'))) return 1;
4033
+      if ((vhostShort && names.has(vhostShort)) || (hostShort && names.has(hostShort))) return 2;
4034
+      return 9;
4035
+    }
4036
+
4037
+    function certMatchesRow(cert, row) {
4038
+      return certRelevance(cert, row) < 9;
4039
+    }
4040
+
4041
+    function compactCertificateLabel(cert, row) {
4042
+      const relevance = certRelevance(cert, row);
4043
+      const id = String(certId(cert));
4044
+      const days = daysUntil(cert.not_after);
4045
+      const suffix = days === null ? '' : ` (${certStatusLabel(days)})`;
4046
+      const timestamp = id.match(/-(\d{14})$/);
4047
+      if (relevance === 0) return `vhost${timestamp ? ' ' + timestamp[1] : ''}${suffix}`;
4048
+      if (relevance === 1) return `host${timestamp ? ' ' + timestamp[1] : ''}${suffix}`;
4049
+      if (relevance === 2) return `alias${timestamp ? ' ' + timestamp[1] : ''}${suffix}`;
4050
+      return `${shortCertificateName(cert)}${suffix}`;
4051
+    }
4052
+
4053
+    function shortCertificateName(cert) {
4054
+      const name = String(cert.common_name || cert.name || cert.id || '');
4055
+      const suffix = '.madagascar.xdev.ro';
4056
+      return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
4057
+    }
4058
+
3989 4059
     function shortAliasForFqdn(name) {
3990 4060
       const suffix = '.madagascar.xdev.ro';
3991 4061
       name = String(name || '').toLowerCase();