@@ -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(); |