Merge pull request #176 from theta42/release-v1.33.0
Release v1.33.0 - Directory Key Badges, Discovered Inventory Merge/Ignore & Desktop Operations
This commit is contained in:
@@ -1,3 +1,13 @@
|
|||||||
|
# v1.33.0 - 2026-08-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Directory Key Badges & Secret Filtering.** Added a gold `🔑 Secret` badge next to resources with stored OpenBao secrets and a `With Secrets` filter checkbox to filter the directory tree by secret presence.
|
||||||
|
- **Kind-Specific Resource Creation Modals.** Added dedicated `openAddSiteModal()`, `openAddHostModal()`, and `openAddServiceModal()` modal handlers for Site, Host, and Service resources.
|
||||||
|
- **Top Toolbar Reorganization.** Updated top tree button to **"+ Add Site"** and removed legacy `Plumbing` slider.
|
||||||
|
- **Optional Child Secret Key Name on Inheritance.** Made key name optional when inheriting parent secrets — automatically defaulting to the original parent secret key name if left blank.
|
||||||
|
- **Discovered Inventory Merge & Ignore Actions.** Added `Merge` (merge IP/interfaces/OS metadata into target resource) and `Ignore` (dismiss discovered item) endpoints (`/api/directory-admin/discovered/merge` & `/api/directory-admin/discovered/ignore`) and table action buttons.
|
||||||
|
- **Agent Tab Telemetry & Desktop Controls.** Rendered Agent Binary Version badge (`v1.8.0`), all physical disks and filesystems table, Active Logged-in Users card, and Desktop Session & Power Operations card (Lock, Display Off, Log Out, Sleep Host).
|
||||||
|
|
||||||
# v1.32.0 - 2026-08-08
|
# v1.32.0 - 2026-08-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -42,12 +42,14 @@ class ThetaAgentDriver extends BaseDriver {
|
|||||||
status: 'online',
|
status: 'online',
|
||||||
driver: this.name,
|
driver: this.name,
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
agentVersion: agent.version,
|
agentVersion: agent.version || 'v1.7.0',
|
||||||
lastSeen: agent.lastSeen,
|
lastSeen: agent.lastSeen,
|
||||||
system: {
|
system: {
|
||||||
cpu: telemetry.cpu || null,
|
cpu: telemetry.cpu || null,
|
||||||
ram: telemetry.memory || null,
|
ram: telemetry.memory || null,
|
||||||
disk: telemetry.disk || null,
|
disk: telemetry.disk || null,
|
||||||
|
disks: telemetry.disks || [],
|
||||||
|
loggedUsers: telemetry.loggedUsers || [],
|
||||||
uptime: telemetry.uptime || null
|
uptime: telemetry.uptime || null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -82,6 +84,16 @@ class ThetaAgentDriver extends BaseDriver {
|
|||||||
return { status: 'ok', driver: this.name, action, result };
|
return { status: 'ok', driver: this.name, action, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['desktop_control', 'lock_session', 'logout_user', 'display_off', 'sleep_host'].includes(action) || subType.startsWith('desktop')) {
|
||||||
|
const subAction = params.subAction || action;
|
||||||
|
const targetUser = params.user || '';
|
||||||
|
const result = await AgentManager.sendCommand(agent.id, 'desktop_control', {
|
||||||
|
subAction,
|
||||||
|
user: targetUser
|
||||||
|
});
|
||||||
|
return { status: 'ok', driver: this.name, action: subAction, result };
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'systemd_action' || subType === 'systemd') {
|
if (action === 'systemd_action' || subType === 'systemd') {
|
||||||
const serviceName = params.serviceName || (resource.metadata && resource.metadata.systemdService) || resource.slug;
|
const serviceName = params.serviceName || (resource.metadata && resource.metadata.systemdService) || resource.slug;
|
||||||
const subAction = params.subAction || action; // start, stop, restart, reload
|
const subAction = params.subAction || action; // start, stop, restart, reload
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"description": "A very simple LDAP management and SSO system",
|
"description": "A very simple LDAP management and SSO system",
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -799,4 +799,61 @@ router.get('/resources/:id/driver-logs', async (req, res, next) => {
|
|||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Discovered Inventory Operations (Merge & Ignore) ───────────────────────
|
||||||
|
router.post('/discovered/ignore', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { resourceId } = req.body;
|
||||||
|
if (!resourceId) return res.status(400).json({ status: 'error', message: 'resourceId is required' });
|
||||||
|
const r = await Resource.get(resourceId);
|
||||||
|
if (!r) return res.status(404).json({ status: 'error', message: 'resource not found' });
|
||||||
|
|
||||||
|
r.metadata = r.metadata || {};
|
||||||
|
r.metadata.ignored = true;
|
||||||
|
await r.save();
|
||||||
|
res.json({ status: 'ok', resourceId: r.id, ignored: true });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/discovered/merge', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { discoveredId, targetId } = req.body;
|
||||||
|
if (!discoveredId || !targetId) {
|
||||||
|
return res.status(400).json({ status: 'error', message: 'discoveredId and targetId are required' });
|
||||||
|
}
|
||||||
|
const disc = await Resource.get(discoveredId);
|
||||||
|
const target = await Resource.get(targetId);
|
||||||
|
if (!disc || !target) {
|
||||||
|
return res.status(404).json({ status: 'error', message: 'Discovered or Target resource not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge metadata (interfaces, discovery sources, OS details)
|
||||||
|
target.metadata = target.metadata || {};
|
||||||
|
disc.metadata = disc.metadata || {};
|
||||||
|
|
||||||
|
const sources = new Set([...(target.metadata.discovery_sources || []), ...(disc.metadata.discovery_sources || [])]);
|
||||||
|
target.metadata.discovery_sources = Array.from(sources);
|
||||||
|
|
||||||
|
if (disc.metadata.interfaces) {
|
||||||
|
const existingInterfaces = target.metadata.interfaces || [];
|
||||||
|
const macs = new Set(existingInterfaces.map(i => i.mac).filter(Boolean));
|
||||||
|
for (const iface of disc.metadata.interfaces) {
|
||||||
|
if (!iface.mac || !macs.has(iface.mac)) {
|
||||||
|
existingInterfaces.push(iface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.metadata.interfaces = existingInterfaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disc.metadata.os) target.metadata.os = target.metadata.os || disc.metadata.os;
|
||||||
|
if (disc.metadata.kernel) target.metadata.kernel = target.metadata.kernel || disc.metadata.kernel;
|
||||||
|
|
||||||
|
await target.save();
|
||||||
|
|
||||||
|
// Remove or mark discovered record as merged
|
||||||
|
await disc.delete();
|
||||||
|
|
||||||
|
res.json({ status: 'ok', mergedTargetId: target.id, targetName: target.name });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
+165
-24
@@ -49,10 +49,10 @@
|
|||||||
<button type="button" class="btn btn-outline-secondary" onclick="collapseAllTree()" title="Collapse all"><i class="fa-solid fa-angles-up"></i></button>
|
<button type="button" class="btn btn-outline-secondary" onclick="collapseAllTree()" title="Collapse all"><i class="fa-solid fa-angles-up"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-switch form-check-inline ms-1 me-1">
|
<div class="form-check form-switch form-check-inline ms-1 me-1">
|
||||||
<input class="form-check-input" type="checkbox" id="toggle-plumbing" onchange="renderTable()">
|
<input class="form-check-input" type="checkbox" id="toggle-secrets-only" onchange="renderTable()">
|
||||||
<label class="form-check-label small text-muted" for="toggle-plumbing" title="Show containers, oauth clients, and sidecars">Plumbing</label>
|
<label class="form-check-label small text-muted" for="toggle-secrets-only" title="Filter tree to show only resources with stored secrets"><i class="fa-solid fa-key text-warning me-1"></i>With Secrets</label>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search..." onkeyup="renderTable()" style="width: 200px;">
|
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search resources or keys..." onkeyup="renderTable()" style="width: 210px;">
|
||||||
<select id="sort-by" class="form-select form-select-sm shadow-sm" onchange="renderTable()" style="width: 150px;">
|
<select id="sort-by" class="form-select form-select-sm shadow-sm" onchange="renderTable()" style="width: 150px;">
|
||||||
<option value="name">Name (A-Z)</option>
|
<option value="name">Name (A-Z)</option>
|
||||||
<option value="kind">Kind</option>
|
<option value="kind">Kind</option>
|
||||||
@@ -68,8 +68,8 @@
|
|||||||
<button class="btn btn-sm btn-outline-primary ms-1 shadow-sm" onclick="openAgentInstallModal()">
|
<button class="btn btn-sm btn-outline-primary ms-1 shadow-sm" onclick="openAgentInstallModal()">
|
||||||
<i class="fa-solid fa-shield-halved me-1"></i> Install Agent
|
<i class="fa-solid fa-shield-halved me-1"></i> Install Agent
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-primary ms-1 shadow-sm" onclick="openAddModal()">
|
<button class="btn btn-sm btn-primary ms-1 shadow-sm" onclick="openAddSiteModal()">
|
||||||
<i class="fas fa-plus"></i> Add Resource
|
<i class="fas fa-plus"></i> Add Site
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +98,7 @@
|
|||||||
<strong>{{name}}</strong>
|
<strong>{{name}}</strong>
|
||||||
</a>
|
</a>
|
||||||
<span class="badge bg-secondary me-1">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
|
<span class="badge bg-secondary me-1">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
|
||||||
|
{{#hasSecret}}<span class="badge bg-warning text-dark me-1" title="Has stored secrets in OpenBao"><i class="fa-solid fa-key me-1"></i>Secret</span>{{/hasSecret}}
|
||||||
{{#metadata.isProduction}}<span class="badge bg-danger me-1">Prod</span>{{/metadata.isProduction}}
|
{{#metadata.isProduction}}<span class="badge bg-danger me-1">Prod</span>{{/metadata.isProduction}}
|
||||||
{{^metadata.isProduction}}<span class="badge bg-info me-1">Dev</span>{{/metadata.isProduction}}
|
{{^metadata.isProduction}}<span class="badge bg-info me-1">Dev</span>{{/metadata.isProduction}}
|
||||||
</td>
|
</td>
|
||||||
@@ -206,9 +207,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-end pe-3">
|
<td class="text-end pe-3">
|
||||||
{{^metadata.managed}}
|
{{^metadata.managed}}
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="promoteResource('{{slug}}')" title="Promote to Managed">
|
<button class="btn btn-sm btn-outline-primary me-1" onclick="promoteResource('{{slug}}')" title="Promote to Managed Directory Host">
|
||||||
<i class="fa-solid fa-arrow-up-right-dots"></i> Promote
|
<i class="fa-solid fa-arrow-up-right-dots"></i> Promote
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-success me-1" onclick="openMergeModal('{{id}}', '{{name}}')" title="Merge into Existing Resource">
|
||||||
|
<i class="fa-solid fa-code-merge"></i> Merge
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="ignoreDiscoveredResource('{{id}}')" title="Ignore / Dismiss">
|
||||||
|
<i class="fa-solid fa-eye-slash"></i> Ignore
|
||||||
|
</button>
|
||||||
{{/metadata.managed}}
|
{{/metadata.managed}}
|
||||||
{{#metadata.managed}}
|
{{#metadata.managed}}
|
||||||
<button class="btn btn-sm btn-outline-secondary" disabled title="Already Managed">
|
<button class="btn btn-sm btn-outline-secondary" disabled title="Already Managed">
|
||||||
@@ -809,9 +816,29 @@
|
|||||||
disksHtml = `<div class="small">Disk Usage: <strong>${fmtNum(t.disk_usage_percent ?? 0)}%</strong> ${bar(t.disk_usage_percent)}</div>`;
|
disksHtml = `<div class="small">Disk Usage: <strong>${fmtNum(t.disk_usage_percent ?? 0)}%</strong> ${bar(t.disk_usage_percent)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agentVer = d.version || t.version || agent.version || 'v1.7.0';
|
||||||
|
const loggedUsers = t.logged_users || d.logged_users || agent.loggedUsers || [];
|
||||||
|
|
||||||
|
let loggedUsersHtml = '';
|
||||||
|
if (loggedUsers.length > 0) {
|
||||||
|
loggedUsersHtml = `<div class="table-responsive"><table class="table table-sm small mb-0 align-middle">
|
||||||
|
<thead><tr><th>User</th><th>Terminal</th><th>Host / IP</th><th>Session Time</th></tr></thead><tbody>` +
|
||||||
|
loggedUsers.map(u => `<tr>
|
||||||
|
<td><i class="fa-solid fa-user text-primary me-1"></i><strong>${esc(u.user)}</strong></td>
|
||||||
|
<td><code class="text-muted">${esc(u.terminal || 'tty')}</code></td>
|
||||||
|
<td>${esc(u.host || 'local')}</td>
|
||||||
|
<td class="text-muted">${u.started ? moment(u.started * 1000).fromNow() : 'active'}</td>
|
||||||
|
</tr>`).join('') + `</tbody></table></div>`;
|
||||||
|
} else {
|
||||||
|
loggedUsersHtml = `<div class="small text-muted p-2"><i class="fa-solid fa-user-slash me-1"></i>No active logged-in user sessions.</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
return `<div class="p-3">
|
return `<div class="p-3">
|
||||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">${esc(agent.name || agent.hostname || 'unknown')} ${online}</h5>
|
<h5 class="mb-0">
|
||||||
|
${esc(agent.name || agent.hostname || 'unknown')} ${online}
|
||||||
|
<span class="badge bg-dark ms-2 font-monospace" title="Theta Agent Binary Version"><i class="fa-solid fa-code-branch me-1"></i>${esc(agentVer)}</span>
|
||||||
|
</h5>
|
||||||
<small class="text-muted">Last seen ${lastSeenStr}</small>
|
<small class="text-muted">Last seen ${lastSeenStr}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -856,18 +883,41 @@
|
|||||||
<!-- Disks Card -->
|
<!-- Disks Card -->
|
||||||
<div class="card mb-3 shadow-sm border">
|
<div class="card mb-3 shadow-sm border">
|
||||||
<div class="card-header bg-light py-2">
|
<div class="card-header bg-light py-2">
|
||||||
<h6 class="mb-0 fw-bold text-dark"><i class="fa-solid fa-hard-drive me-1"></i> Disks & Storage</h6>
|
<h6 class="mb-0 fw-bold text-dark"><i class="fa-solid fa-hard-drive me-1"></i> Physical Disks & Filesystems</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
${disksHtml}
|
${disksHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Logged-in Users Card -->
|
||||||
|
<div class="card mb-3 shadow-sm border">
|
||||||
|
<div class="card-header bg-light py-2">
|
||||||
|
<h6 class="mb-0 fw-bold text-dark"><i class="fa-solid fa-users me-1"></i> Active Logged-in Users</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
${loggedUsersHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-6">GPU <strong>${gpu}</strong></div>
|
<div class="col-6">GPU <strong>${gpu}</strong></div>
|
||||||
<div class="col-6">ZFS <strong>${esc(t.zfs_health || 'N/A')}</strong></div>
|
<div class="col-6">ZFS <strong>${esc(t.zfs_health || 'N/A')}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Controls Card -->
|
||||||
|
<div class="card mb-3 border-info shadow-sm">
|
||||||
|
<div class="card-header bg-info text-dark py-2">
|
||||||
|
<h6 class="mb-0 fw-bold"><i class="fa-solid fa-desktop me-1"></i> Desktop Session & Power Controls</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-3 d-flex flex-wrap gap-2">
|
||||||
|
<button class="btn btn-outline-dark btn-sm" onclick="agentDesktopControl('${agent.id}', 'lock')"><i class="fa-solid fa-lock me-1"></i> Lock Session</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="agentDesktopControl('${agent.id}', 'display_off')"><i class="fa-solid fa-tv me-1"></i> Display Off</button>
|
||||||
|
<button class="btn btn-outline-warning btn-sm" onclick="agentDesktopControl('${agent.id}', 'logout')"><i class="fa-solid fa-right-from-bracket me-1"></i> Log Out User</button>
|
||||||
|
<button class="btn btn-outline-primary btn-sm" onclick="agentDesktopControl('${agent.id}', 'sleep')"><i class="fa-solid fa-moon me-1"></i> Sleep Host</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Host Power Controls -->
|
<!-- Host Power Controls -->
|
||||||
<div class="card mb-3 border-danger shadow-sm">
|
<div class="card mb-3 border-danger shadow-sm">
|
||||||
<div class="card-header bg-danger text-white py-2">
|
<div class="card-header bg-danger text-white py-2">
|
||||||
@@ -1041,22 +1091,21 @@
|
|||||||
function renderTable() {
|
function renderTable() {
|
||||||
const filter = $('#search-filter').val().toLowerCase();
|
const filter = $('#search-filter').val().toLowerCase();
|
||||||
const sort = $('#sort-by').val();
|
const sort = $('#sort-by').val();
|
||||||
const showPlumbing = $('#toggle-plumbing').is(':checked');
|
const secretsOnly = $('#toggle-secrets-only').is(':checked');
|
||||||
|
|
||||||
let filtered = rawResources.filter(r => {
|
let filtered = rawResources.filter(r => {
|
||||||
if (!showPlumbing && !filter) {
|
if (secretsOnly && !r.hasSecret) {
|
||||||
const sub = (r.metadata?.subType || '').toLowerCase();
|
return false;
|
||||||
if (r.kind === 'container' || r.kind === 'oauth' || sub === 'sidecar' || sub === 'container' || sub === 'openresty') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!filter) return true;
|
if (!filter) return true;
|
||||||
|
const secretKeysStr = (r.secretKeys || []).join(' ').toLowerCase();
|
||||||
return (r.name || '').toLowerCase().includes(filter) ||
|
return (r.name || '').toLowerCase().includes(filter) ||
|
||||||
(r.slug || '').toLowerCase().includes(filter) ||
|
(r.slug || '').toLowerCase().includes(filter) ||
|
||||||
(r.kind || '').toLowerCase().includes(filter) ||
|
(r.kind || '').toLowerCase().includes(filter) ||
|
||||||
(r.metadata?.subType || '').toLowerCase().includes(filter) ||
|
(r.metadata?.subType || '').toLowerCase().includes(filter) ||
|
||||||
(r.metadata?.ip || '').toLowerCase().includes(filter) ||
|
(r.metadata?.ip || '').toLowerCase().includes(filter) ||
|
||||||
(r.hostName || '').toLowerCase().includes(filter);
|
(r.hostName || '').toLowerCase().includes(filter) ||
|
||||||
|
secretKeysStr.includes(filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
@@ -1352,6 +1401,96 @@
|
|||||||
populateHostDropdown(parentId || '');
|
populateHostDropdown(parentId || '');
|
||||||
toggleFormFields();
|
toggleFormFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openAddSiteModal() {
|
||||||
|
openAddModal(null, null);
|
||||||
|
$('#res-kind').val('site');
|
||||||
|
toggleFormFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddHostModal(parentId) {
|
||||||
|
openAddModal(parentId, 'site');
|
||||||
|
$('#res-kind').val('host');
|
||||||
|
toggleFormFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddServiceModal(parentId) {
|
||||||
|
openAddModal(parentId, 'host');
|
||||||
|
$('#res-kind').val('service');
|
||||||
|
toggleFormFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openMergeModal(discoveredId, discName) {
|
||||||
|
const targets = rawResources.filter(r => r.kind === 'host' || r.kind === 'site' || r.kind === 'service');
|
||||||
|
let optionsHtml = targets.map(r => `<option value="${r.id}">${esc(r.name)} (${esc(r.kind)}${r.metadata?.subType ? ' - ' + esc(r.metadata.subType) : ''})</option>`).join('');
|
||||||
|
|
||||||
|
app.modal.open({
|
||||||
|
title: `Merge '${esc(discName)}' into Existing Resource`,
|
||||||
|
size: 'md',
|
||||||
|
bodyHtml: `
|
||||||
|
<div class="p-3">
|
||||||
|
<p class="small text-muted">Select an existing Directory resource to merge IP addresses, network interfaces, and OS telemetry into:</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold">Target Directory Resource</label>
|
||||||
|
<select class="form-select" id="merge-target-id">
|
||||||
|
${optionsHtml}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
footer: {
|
||||||
|
buttonsHtml: `
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="app.modal.close()">Cancel</button>
|
||||||
|
<button class="btn btn-success btn-sm" onclick="submitMergeResource('${discoveredId}')"><i class="fa-solid fa-code-merge me-1"></i> Confirm Merge</button>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitMergeResource(discoveredId) {
|
||||||
|
const targetId = $('#merge-target-id').val();
|
||||||
|
if (!targetId) return;
|
||||||
|
try {
|
||||||
|
app.messages.action('Merging discovered resource...', $('#app-modal-body'), 'info');
|
||||||
|
await app.api.post('directory-admin/discovered/merge', { discoveredId, targetId });
|
||||||
|
app.messages.action('Resource merged successfully!', null, 'success');
|
||||||
|
app.modal.close();
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
app.messages.action(err.message || 'Failed to merge resource', $('#app-modal-body'), 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ignoreDiscoveredResource(resourceId) {
|
||||||
|
const confirmed = await app.messages.confirm('Ignore and dismiss this discovered device?', null, 'warning');
|
||||||
|
if (!confirmed) return;
|
||||||
|
try {
|
||||||
|
await app.api.post('directory-admin/discovered/ignore', { resourceId });
|
||||||
|
app.messages.action('Discovered device ignored.', null, 'info');
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
app.messages.action(err.message || 'Failed to ignore device', null, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function agentDesktopControl(agentId, action, username = '') {
|
||||||
|
const confirmed = await app.messages.confirm(`Execute '${action}' desktop control action?`, $('#res-modal'), 'warning');
|
||||||
|
if (!confirmed) return;
|
||||||
|
const resourceId = $('#res-id').val();
|
||||||
|
if (!resourceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
app.messages.action(`Sending '${action}' desktop command to agent...`, $('#res-modal'), 'info');
|
||||||
|
const res = await app.api.post(`directory-admin/resources/${resourceId}/driver-action`, {
|
||||||
|
action: 'desktop_control',
|
||||||
|
subAction: action,
|
||||||
|
user: username
|
||||||
|
});
|
||||||
|
app.messages.action(`Desktop action '${action}' completed successfully!`, $('#res-modal'), 'success');
|
||||||
|
} catch (err) {
|
||||||
|
app.messages.action(err.message || 'Desktop action failed', $('#res-modal'), 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var ldapGroupsCache = null;
|
var ldapGroupsCache = null;
|
||||||
async function loadLdapGroups() {
|
async function loadLdapGroups() {
|
||||||
@@ -1753,22 +1892,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function inheritParentSecret() {
|
async function inheritParentSecret() {
|
||||||
const childKey = $('#inherit-child-key').val().trim();
|
let childKey = $('#inherit-child-key').val().trim();
|
||||||
const inheritVal = $('#inherit-parent-select').val();
|
const inheritVal = $('#inherit-parent-select').val();
|
||||||
const resourceId = $('#res-id').val();
|
const resourceId = $('#res-id').val();
|
||||||
|
|
||||||
if (!childKey) {
|
|
||||||
app.messages.action('Please enter a child secret key name (e.g. DB_HOST).', $('#secrets-tab-container'), 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!SECRET_KEY_REGEX.test(childKey)) {
|
|
||||||
app.messages.action('Invalid child key name. Only letters, numbers, and underscores allowed.', $('#secrets-tab-container'), 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!inheritVal) {
|
if (!inheritVal) {
|
||||||
app.messages.action('Select a parent secret to inherit from.', $('#secrets-tab-container'), 'warning');
|
app.messages.action('Select a parent secret to inherit from.', $('#secrets-tab-container'), 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!childKey) {
|
||||||
|
const parts = inheritVal.split(':');
|
||||||
|
childKey = parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SECRET_KEY_REGEX.test(childKey)) {
|
||||||
|
app.messages.action('Invalid child key name. Only letters, numbers, and underscores allowed.', $('#secrets-tab-container'), 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!resourceId) return;
|
if (!resourceId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user