chore(agents): label the Install Agent modal fields as Theta Directory URL
This commit is contained in:
+54
-54
@@ -1,4 +1,4 @@
|
||||
<%- include('top') %>
|
||||
<%- include('top') %>
|
||||
|
||||
<style>
|
||||
/* The caret's rotation is driven by a class on the BUTTON, not by swapping
|
||||
@@ -63,7 +63,7 @@
|
||||
</select>
|
||||
<div class="input-group input-group-sm shadow-sm" style="width: 230px;">
|
||||
<span class="input-group-text" title="What can this user reach?"><i class="fa-solid fa-user-shield"></i></span>
|
||||
<input type="text" id="user-access-uid" class="form-control" placeholder="uid…" list="access-uid-list"
|
||||
<input type="text" id="user-access-uid" class="form-control" placeholder="uid…" list="access-uid-list"
|
||||
onkeydown="if(event.key==='Enter'){openUserAccessModal();}">
|
||||
<datalist id="access-uid-list"></datalist>
|
||||
<button class="btn btn-outline-secondary" onclick="openUserAccessModal()">Check</button>
|
||||
@@ -314,14 +314,14 @@
|
||||
<label class="form-label font-weight-bold"><i class="fa-solid fa-wand-magic-sparkles me-1 text-primary"></i>Preset Subtype Template</label>
|
||||
<select id="res-subtype-template" class="form-select shadow-sm me-2 mb-2" onchange="applySubtypeTemplate()">
|
||||
<option value="">-- Custom / Manual --</option>
|
||||
<option value="docker">📦 Docker Engine / Container</option>
|
||||
<option value="systemd">⚙️ Linux Systemd Service</option>
|
||||
<option value="postgresql">🗄️ PostgreSQL Database</option>
|
||||
<option value="redis">🔴 Redis In-Memory Cache</option>
|
||||
<option value="proxmox">🖥️ Proxmox VE Hypervisor</option>
|
||||
<option value="wireguard">🛡️ WireGuard VPN Tunnel</option>
|
||||
<option value="unifi">📶 UniFi Network Controller</option>
|
||||
<option value="k8s">☸️ Kubernetes Cluster Node</option>
|
||||
<option value="docker">📦 Docker Engine / Container</option>
|
||||
<option value="systemd">âš™ï¸ Linux Systemd Service</option>
|
||||
<option value="postgresql">ðŸ—„ï¸ PostgreSQL Database</option>
|
||||
<option value="redis">🔴 Redis In-Memory Cache</option>
|
||||
<option value="proxmox">ðŸ–¥ï¸ Proxmox VE Hypervisor</option>
|
||||
<option value="wireguard">ðŸ›¡ï¸ WireGuard VPN Tunnel</option>
|
||||
<option value="unifi">📶 UniFi Network Controller</option>
|
||||
<option value="k8s">â˜¸ï¸ Kubernetes Cluster Node</option>
|
||||
</select>
|
||||
<label class="form-label">Sub Type</label>
|
||||
<input type="text" id="res-subtype" class="form-control shadow-sm" placeholder="e.g. docker, systemd, postgresql...">
|
||||
@@ -602,7 +602,7 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning border-0 shadow-sm p-2 small mt-2 mb-0" id="gen-secret-notice" style="display:none">
|
||||
<i class="fa-solid fa-shield-halved text-warning me-1"></i> Generated secret is shown in the field above. Click <strong>Save Secret</strong> to store in OpenBao — secret values will not be displayed again once saved.
|
||||
<i class="fa-solid fa-shield-halved text-warning me-1"></i> Generated secret is shown in the field above. Click <strong>Save Secret</strong> to store in OpenBao — secret values will not be displayed again once saved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -718,7 +718,7 @@
|
||||
|
||||
rawResources = [];
|
||||
for (const r of resResources.results) {
|
||||
r.hostName = '—';
|
||||
r.hostName = '—';
|
||||
r.parentId = null;
|
||||
const parentEdge = allEdges.find(e => e.childId === r.id);
|
||||
if (parentEdge) {
|
||||
@@ -770,13 +770,13 @@
|
||||
}
|
||||
if (a.revoked) { n.agentColor = '#6c757d'; n.agentStatusTitle = 'Agent enrollment revoked'; return; }
|
||||
if (!a.isOnline) {
|
||||
const seen = (a.lastSeen || a.last_seen) ? ' — last seen ' + timeAgo(a.lastSeen || new Date(a.last_seen * 1000).toISOString()) : '';
|
||||
const seen = (a.lastSeen || a.last_seen) ? ' — last seen ' + timeAgo(a.lastSeen || new Date(a.last_seen * 1000).toISOString()) : '';
|
||||
n.agentColor = '#dc3545'; n.agentStatusTitle = 'Agent enrolled but offline' + seen; return;
|
||||
}
|
||||
const t = a.lastTelemetry || {};
|
||||
const high = (t.cpu_usage_percent > 80) || (t.ram_usage_percent > 80) || (t.disk_usage_percent > 90);
|
||||
n.agentColor = high ? '#ffc107' : '#198754';
|
||||
n.agentStatusTitle = high ? 'Connected — high load' : 'Connected — healthy';
|
||||
n.agentStatusTitle = high ? 'Connected — high load' : 'Connected — healthy';
|
||||
}
|
||||
|
||||
// Agent tab body for the resource modal.
|
||||
@@ -1112,7 +1112,7 @@
|
||||
async function openUserAccessModal() {
|
||||
const uid = ($('#user-access-uid').val() || '').trim();
|
||||
if (!uid) return;
|
||||
app.modal.open({ title: 'Access for ' + uid, bodyHtml: '<p class="text-muted">Loading…</p>' });
|
||||
app.modal.open({ title: 'Access for ' + uid, bodyHtml: '<p class="text-muted">Loading…</p>' });
|
||||
try {
|
||||
const res = await app.api.get('directory-admin/user-access/' + encodeURIComponent(uid));
|
||||
const data = res.results;
|
||||
@@ -1202,7 +1202,7 @@
|
||||
}
|
||||
n.indentHtml = indentHtml;
|
||||
n.depth = depth;
|
||||
n.displayName = (n.name && n.name.length > 16) ? n.name.substring(0, 16) + '…' : (n.name || '');
|
||||
n.displayName = (n.name && n.name.length > 16) ? n.name.substring(0, 16) + '…' : (n.name || '');
|
||||
// A leaf gets a spacer of the same width, so names stay aligned down
|
||||
// the column instead of jittering by whether a row has children.
|
||||
n.caretHtml = n.children.length
|
||||
@@ -1231,7 +1231,7 @@
|
||||
applyTreeCollapse();
|
||||
}
|
||||
|
||||
// ── Collapsible tree ───────────────────────────────────────────────────────
|
||||
// ── Collapsible tree ───────────────────────────────────────────────────────
|
||||
// Which nodes are collapsed, by resource id. Persisted so the shape of the
|
||||
// tree survives a refresh (and the Directory self-heal reload that follows
|
||||
// most edits) -- a tree that re-expands every time is worse than no tree.
|
||||
@@ -1506,14 +1506,14 @@
|
||||
<div class="p-3">
|
||||
<p class="small text-muted mb-2">Search and select an existing Directory resource to merge IP addresses, network interfaces, and OS telemetry into:</p>
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control mb-2 shadow-sm" id="merge-search-input" placeholder="🔍 Type to filter resources..." oninput="filterMergeTargets()">
|
||||
<input type="text" class="form-control mb-2 shadow-sm" id="merge-search-input" placeholder="🔠Type to filter resources..." oninput="filterMergeTargets()">
|
||||
<input type="hidden" id="merge-target-id" value="${targets[0] ? targets[0].id : ''}">
|
||||
<div class="list-group overflow-auto border rounded shadow-sm" id="merge-targets-list" style="max-height: 240px;">
|
||||
${targets.map((r, i) => `
|
||||
<button type="button" class="list-group-item list-group-item-action p-2 merge-item-btn ${i===0 ? 'active' : ''}" data-id="${r.id}" onclick="selectMergeTarget(this)">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>${esc(r.name)}</strong>
|
||||
<span class="badge bg-secondary">${esc(r.kind)}${r.metadata?.subType ? ' · ' + esc(r.metadata.subType) : ''}</span>
|
||||
<span class="badge bg-secondary">${esc(r.kind)}${r.metadata?.subType ? ' · ' + esc(r.metadata.subType) : ''}</span>
|
||||
</div>
|
||||
${r.metadata?.ip ? `<small class="font-monospace text-muted d-block">${esc(r.metadata.ip)}</small>` : ''}
|
||||
</button>
|
||||
@@ -1915,7 +1915,7 @@
|
||||
$select.append('<option value="">-- Select Parent Resource Secret --</option>');
|
||||
currentParentSecretsList.forEach(p => {
|
||||
const valStr = `INHERIT:${p.parentSlug}:${p.key}`;
|
||||
const labelStr = `${p.parentName || p.parentSlug} → ${p.key}`;
|
||||
const labelStr = `${p.parentName || p.parentSlug} → ${p.key}`;
|
||||
$select.append(`<option value="${esc(valStr)}">${esc(labelStr)}</option>`);
|
||||
});
|
||||
}
|
||||
@@ -2064,7 +2064,7 @@
|
||||
await loadResources();
|
||||
loadDiscoveryResources();
|
||||
app.modal.close();
|
||||
app.messages.toast('Promoted ' + slug + (res && res.groups ? ' — created groups: ' + res.groups.join(', ') : ''), 'success');
|
||||
app.messages.toast('Promoted ' + slug + (res && res.groups ? ' — created groups: ' + res.groups.join(', ') : ''), 'success');
|
||||
} catch (err) {
|
||||
promoteSlug = slug;
|
||||
app.messages.action('Failed to promote: ' + (err.message || err), app.modal.body(), 'danger');
|
||||
@@ -2232,7 +2232,7 @@
|
||||
|
||||
async function deleteResource(id) {
|
||||
// Called from the outer table's row button, not from inside the resource
|
||||
// modal — target the page's own card so the confirm/error renders
|
||||
// modal — target the page's own card so the confirm/error renders
|
||||
// somewhere actually visible.
|
||||
const $target = $('#resources-list');
|
||||
const ok = await app.messages.confirm('Are you sure you want to delete this resource? All relationships will be destroyed.', $target, 'danger');
|
||||
@@ -2343,14 +2343,14 @@
|
||||
joinCmd = `curl -fsSL ${joinUrl}/resources/theta-agent/install.sh | sh -s -- --url "${joinUrl}" --join-key "${mintedJoinKey}"`;
|
||||
} else if (selectedKeyId) {
|
||||
const k = agentJoinKeys.find(x => x.id === selectedKeyId);
|
||||
joinCmd = `curl -fsSL ${joinUrl}/resources/theta-agent/install.sh | sh -s -- --url "${joinUrl}" --join-key "${k ? k.keyPrefix : ''}…"\n\n# Paste the full value of this key -- it was only shown when created.\n# If you no longer have it, create a new key above.`;
|
||||
joinCmd = `curl -fsSL ${joinUrl}/resources/theta-agent/install.sh | sh -s -- --url "${joinUrl}" --join-key "${k ? k.keyPrefix : ''}…"\n\n# Paste the full value of this key -- it was only shown when created.\n# If you no longer have it, create a new key above.`;
|
||||
} else {
|
||||
joinCmd = '# Create a join key above, or select one you already have the value for.';
|
||||
}
|
||||
$('#agent-join-command').text(joinCmd);
|
||||
|
||||
// Windows: the installer is a GitHub release artifact (built by the
|
||||
// theta-agent release workflow — nothing binary lives in this repo). It
|
||||
// theta-agent release workflow — nothing binary lives in this repo). It
|
||||
// takes the same values as /SERVER_URL /JOIN_KEY command-line params.
|
||||
const WIN_SETUP = 'theta-agent-2.1.0-windows-amd64-setup.exe';
|
||||
const WIN_SETUP_URL = `https://github.com/theta42/theta-agent/releases/latest/download/${WIN_SETUP}`;
|
||||
@@ -2473,7 +2473,7 @@
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mb-3">
|
||||
<!-- ── Join key: one credential, host enrolls itself ────────────── -->
|
||||
<!-- ── Join key: one credential, host enrolls itself ────────────── -->
|
||||
<div class="tab-pane fade show active" id="agent-mode-join" role="tabpanel">
|
||||
<div class="card border-success">
|
||||
<div class="card-header py-2 fw-bold small bg-success-subtle">
|
||||
@@ -2483,14 +2483,14 @@
|
||||
<p class="small text-muted mb-3">
|
||||
Run this on any host and it enrolls itself. The SSO issues that host its own
|
||||
token and public key on first connect, and the agent writes both into its
|
||||
<code>agent.yml</code> — nothing to copy back and forth. One key works for as
|
||||
<code>agent.yml</code> — nothing to copy back and forth. One key works for as
|
||||
many hosts as you like; each still gets its own revocable identity.
|
||||
</p>
|
||||
<div class="d-flex gap-2 align-items-end mb-3">
|
||||
<div class="flex-grow-1">
|
||||
<label class="form-label small fw-bold mb-1">Existing join keys</label>
|
||||
<select id="agent-join-key-select" class="form-select form-select-sm" onchange="updateAgentCommands()"></select>
|
||||
<div class="form-text small">A key's value is shown only when it is created — mint a new one if you don't have it saved.</div>
|
||||
<div class="form-text small">A key's value is shown only when it is created — mint a new one if you don't have it saved.</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-success" onclick="mintAgentJoinKey()">
|
||||
<i class="fa-solid fa-plus me-1"></i> New join key
|
||||
@@ -2536,7 +2536,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Pre-register: bind to a host resource up front ───────────── -->
|
||||
<!-- ── Pre-register: bind to a host resource up front ───────────── -->
|
||||
<div class="tab-pane fade" id="agent-mode-pre" role="tabpanel">
|
||||
|
||||
<div class="card border-primary mb-3" id="agent-enroll-card">
|
||||
@@ -2583,11 +2583,11 @@
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="agent-install-tab-content">
|
||||
<!-- ── Tab 1: Quick Install ──────────────────────────────────────── -->
|
||||
<!-- ── Tab 1: Quick Install ──────────────────────────────────────── -->
|
||||
<div class="tab-pane fade show active" id="tab-quick-pane" role="tabpanel">
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold mb-1">SSO Server URL</label>
|
||||
<label class="form-label small fw-bold mb-1">Theta Directory URL</label>
|
||||
<input type="text" id="agent-quick-url" class="form-control form-control-sm" value="${currentOrigin}" oninput="updateAgentCommands()">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@@ -2616,11 +2616,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Tab 2: Custom Config Wizard ────────────────────────────────── -->
|
||||
<!-- ── Tab 2: Custom Config Wizard ────────────────────────────────── -->
|
||||
<div class="tab-pane fade" id="tab-custom-pane" role="tabpanel">
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small fw-bold mb-1">SSO Server URL</label>
|
||||
<label class="form-label small fw-bold mb-1">Theta Directory URL</label>
|
||||
<input type="text" id="agent-custom-url" class="form-control form-control-sm" value="${currentOrigin}" oninput="updateAgentCommands()">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@@ -2717,12 +2717,12 @@
|
||||
// Only hosts can carry an agent -- the API rejects anything else, so don't
|
||||
// offer it here.
|
||||
const $sel = $('#agent-enroll-resource').empty();
|
||||
$sel.append('<option value="">(not bound — bind later)</option>');
|
||||
$sel.append('<option value="">(not bound — bind later)</option>');
|
||||
rawResources
|
||||
.filter(r => r.kind === 'host')
|
||||
.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
|
||||
.forEach(r => {
|
||||
const taken = agentsByResource[r.id] ? ' — already has an agent' : '';
|
||||
const taken = agentsByResource[r.id] ? ' — already has an agent' : '';
|
||||
$sel.append($('<option>').val(r.id).text((r.name || r.slug) + taken).prop('disabled', !!agentsByResource[r.id]));
|
||||
});
|
||||
|
||||
@@ -2749,12 +2749,12 @@
|
||||
|
||||
const $sel = $('#agent-join-key-select').empty();
|
||||
if (!agentJoinKeys.length) {
|
||||
$sel.append('<option value="">No join keys yet — create one</option>');
|
||||
$sel.append('<option value="">No join keys yet — create one</option>');
|
||||
} else {
|
||||
$sel.append('<option value="">Select a key…</option>');
|
||||
$sel.append('<option value="">Select a key…</option>');
|
||||
agentJoinKeys.forEach(k => {
|
||||
const used = k.use_count ? `${k.use_count} host${k.use_count === 1 ? '' : 's'}` : 'unused';
|
||||
$sel.append($('<option>').val(k.id).text(`${k.label} (${k.keyPrefix}…, ${used})`));
|
||||
$sel.append($('<option>').val(k.id).text(`${k.label} (${k.keyPrefix}…, ${used})`));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2764,7 +2764,7 @@
|
||||
$tbody.append('<tr><td colspan="6" class="text-muted">No join keys yet.</td></tr>');
|
||||
} else {
|
||||
agentJoinKeysAll.forEach(k => {
|
||||
const created = k.created_on ? new Date(k.created_on * 1000).toLocaleDateString() : '—';
|
||||
const created = k.created_on ? new Date(k.created_on * 1000).toLocaleDateString() : '—';
|
||||
const used = k.use_count ? `${k.use_count} host${k.use_count === 1 ? '' : 's'}` : '0 hosts';
|
||||
const status = k.revoked
|
||||
? '<span class="badge bg-secondary">Revoked</span>'
|
||||
@@ -2773,7 +2773,7 @@
|
||||
`<button class="btn btn-outline-warning btn-sm" title="Revoke -- stops it enrolling new hosts; already-joined hosts are unaffected" onclick="confirmAgentJoinKeyAction(this, '${k.id}', 'revoke')"><i class="fa-solid fa-ban"></i></button>`;
|
||||
const $row = $('<tr>').attr('data-join-key-row', k.id).append(
|
||||
$('<td>').text(k.label),
|
||||
$('<td>').append($('<code>').text(k.keyPrefix + '…')),
|
||||
$('<td>').append($('<code>').text(k.keyPrefix + '…')),
|
||||
$('<td>').text(created),
|
||||
$('<td>').append($('<a href="#">').text(used).on('click', function(e) { e.preventDefault(); viewAgentJoinKeyHosts(k.id, k.label); })),
|
||||
$('<td>').html(status),
|
||||
@@ -2792,7 +2792,7 @@
|
||||
}
|
||||
|
||||
async function viewAgentJoinKeyHosts(id, label) {
|
||||
const $out = $('#agent-join-key-hosts').show().html('<i class="fa-solid fa-spinner fa-spin"></i> Loading…');
|
||||
const $out = $('#agent-join-key-hosts').show().html('<i class="fa-solid fa-spinner fa-spin"></i> Loading…');
|
||||
try {
|
||||
const res = await app.api.get(`agent/join-keys/${id}/agents`);
|
||||
const body = (res && (res.results || res)) || {};
|
||||
@@ -2804,7 +2804,7 @@
|
||||
const rows = agents.map(a => {
|
||||
const dot = a.isOnline ? 'text-success' : 'text-muted';
|
||||
const seen = a.last_seen ? new Date(a.last_seen * 1000).toLocaleString() : 'never';
|
||||
return `<tr><td><i class="fa-solid fa-circle ${dot}" style="font-size:8px"></i> ${esc(a.name)}</td><td>${esc(a.enrolled_on ? new Date(a.enrolled_on * 1000).toLocaleDateString() : '—')}</td><td>${esc(seen)}</td></tr>`;
|
||||
return `<tr><td><i class="fa-solid fa-circle ${dot}" style="font-size:8px"></i> ${esc(a.name)}</td><td>${esc(a.enrolled_on ? new Date(a.enrolled_on * 1000).toLocaleDateString() : '—')}</td><td>${esc(seen)}</td></tr>`;
|
||||
}).join('');
|
||||
$out.html(
|
||||
`<div class="small fw-bold mb-1">Hosts joined with ${esc(label)}:</div>` +
|
||||
@@ -2857,7 +2857,7 @@
|
||||
$('#agent-join-key-result').show().html(
|
||||
'<div class="alert alert-success py-2 small mb-3">'
|
||||
+ '<i class="fa-solid fa-circle-check me-1"></i><strong>Join key created.</strong> '
|
||||
+ 'It is shown <strong>once</strong> — only its hash is stored. It is already in the command below.'
|
||||
+ 'It is shown <strong>once</strong> — only its hash is stored. It is already in the command below.'
|
||||
+ '</div>'
|
||||
+ '<label class="form-label small fw-bold mb-1">Join key</label>'
|
||||
+ '<div class="input-group input-group-sm mb-3">'
|
||||
@@ -2881,7 +2881,7 @@
|
||||
app.messages.toast('Give the agent a name first.', 'warning');
|
||||
return;
|
||||
}
|
||||
const $btn = $('#agent-enroll-btn').prop('disabled', true).html('<i class="fa-solid fa-spinner fa-spin me-1"></i> Enrolling…');
|
||||
const $btn = $('#agent-enroll-btn').prop('disabled', true).html('<i class="fa-solid fa-spinner fa-spin me-1"></i> Enrolling…');
|
||||
try {
|
||||
const res = await app.api.post('agent/enroll', { name, resourceId });
|
||||
const body = res && (res.results || res);
|
||||
@@ -2904,7 +2904,7 @@
|
||||
keyWarn +
|
||||
'<div class="alert alert-success py-2 small mb-2">'
|
||||
+ '<i class="fa-solid fa-circle-check me-1"></i><strong>Enrolled.</strong> '
|
||||
+ 'This token is shown <strong>once</strong> — only its hash is stored. '
|
||||
+ 'This token is shown <strong>once</strong> — only its hash is stored. '
|
||||
+ 'If you lose it, rotate the agent to issue a new one.</div>'
|
||||
+ '<label class="form-label small fw-bold mb-1">Agent token</label>'
|
||||
+ '<div class="input-group input-group-sm mb-2">'
|
||||
@@ -2996,7 +2996,7 @@
|
||||
: '';
|
||||
const log = p.lastLog || '(no log captured for this run)';
|
||||
app.modal.open({
|
||||
title: 'Run log — ' + (p.name || p.slug),
|
||||
title: 'Run log — ' + (p.name || p.slug),
|
||||
size: 'lg',
|
||||
bodyHtml: body + '<pre class="p-2 mb-0 bg-light border" style="max-height:55vh;overflow:auto;white-space:pre-wrap;font-size:.85rem;">' + esc(log) + '</pre>',
|
||||
});
|
||||
@@ -3025,16 +3025,16 @@
|
||||
|
||||
var discoveryPluginTypes = [];
|
||||
|
||||
// ── Discovery plugin config helpers (ported from plugins.ejs) ─────────────
|
||||
// ── Discovery plugin config helpers (ported from plugins.ejs) ─────────────
|
||||
// Stored value is always a 5-field cron string; the dropdown picks a preset
|
||||
// and "Custom…" reveals the raw input. Config fields are driven by each
|
||||
// and "Custom…" reveals the raw input. Config fields are driven by each
|
||||
// plugin type's configSchema so per-plugin settings (e.g. Proxmox url /
|
||||
// tokenId / tokenSecret) are collected at create time.
|
||||
var DP_CRON_PRESETS = [
|
||||
{ key: 'hourly', label: 'Hourly', cron: '0 * * * *' },
|
||||
{ key: 'daily', label: 'Daily (midnight)', cron: '0 0 * * *' },
|
||||
{ key: 'weekly', label: 'Weekly (Sun)', cron: '0 0 * * 0' },
|
||||
{ key: 'custom', label: 'Custom…', cron: null },
|
||||
{ key: 'custom', label: 'Custom…', cron: null },
|
||||
];
|
||||
function dpCronKeyFor(cron) {
|
||||
var m = DP_CRON_PRESETS.filter(function(p){ return p.cron === cron; })[0];
|
||||
@@ -3107,7 +3107,7 @@
|
||||
var ph = f.placeholder ? (' placeholder="' + esc(f.placeholder) + '"') : '';
|
||||
var val = '';
|
||||
if (!f.secret && values[f.key] != null) val = ' value="' + esc(values[f.key]) + '"';
|
||||
if (f.secret && values.__isEdit) ph = ' placeholder="unchanged — type a new value to replace"';
|
||||
if (f.secret && values.__isEdit) ph = ' placeholder="unchanged — type a new value to replace"';
|
||||
html += '<div class="mb-3"><label class="form-label fw-bold">' + label + '</label>' +
|
||||
'<input type="' + inputType + '" class="form-control" id="' + prefix + f.key + '"' + req + ph + val + '></div>';
|
||||
}
|
||||
@@ -3194,7 +3194,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Plugin Type</label>
|
||||
<input type="text" class="form-control" value="${esc(p.pluginType)}" disabled>
|
||||
<div class="form-text">The type is fixed once an instance exists — create a new instance to use a different one.</div>
|
||||
<div class="form-text">The type is fixed once an instance exists — create a new instance to use a different one.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Instance Name</label>
|
||||
@@ -3216,7 +3216,7 @@
|
||||
<button class="btn btn-primary" onclick="saveEditedDiscoveryPlugin('${p.id}')">Save changes</button>
|
||||
</div>
|
||||
`;
|
||||
app.modal.open({ title: 'Edit Discovery Plugin — ' + p.name, bodyHtml: bodyHtml, size: 'lg' });
|
||||
app.modal.open({ title: 'Edit Discovery Plugin — ' + p.name, bodyHtml: bodyHtml, size: 'lg' });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3301,7 +3301,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Multi-Site & Master Node Controls ─────────────────────────────────────
|
||||
// ── Multi-Site & Master Node Controls ─────────────────────────────────────
|
||||
async function refreshSiteStatus() {
|
||||
try {
|
||||
const res = await app.api.get('directory-admin/site-status');
|
||||
@@ -3329,7 +3329,7 @@
|
||||
'<div class="card mb-3 shadow-sm border-' + (isMaster ? 'warning' : 'info') + '">' +
|
||||
'<div class="card-body">' +
|
||||
'<h5 class="card-title d-flex align-items-center justify-content-between">' +
|
||||
'<span>' + (isMaster ? '👑 <strong>Master Site Node</strong>' : '⚡ <strong>Spoke Site Node</strong>') + '</span>' +
|
||||
'<span>' + (isMaster ? '👑 <strong>Master Site Node</strong>' : '⚡ <strong>Spoke Site Node</strong>') + '</span>' +
|
||||
'<span class="badge bg-' + (isMaster ? 'warning text-dark' : 'info text-dark') + '">' + esc(cfg.siteMode || 'master') + '</span>' +
|
||||
'</h5>' +
|
||||
'<p class="card-text text-muted small mb-2">Multi-site directory & replication state for this node.</p>' +
|
||||
@@ -3388,7 +3388,7 @@
|
||||
loadDiscoveryPlugins();
|
||||
refreshSiteStatus();
|
||||
// Keep the host status dots live: refresh the agent join periodically and on
|
||||
// socket.io agent.* broadcasts (dedicated socket — the app default is P2PSub).
|
||||
// socket.io agent.* broadcasts (dedicated socket — the app default is P2PSub).
|
||||
refreshAgents();
|
||||
setInterval(refreshAgents, 30000);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user