release: v1.33.0 - Directory Key Badges, Discovered Inventory Merge/Ignore & Desktop Operations

This commit is contained in:
2026-08-08 18:12:37 -04:00
parent fc9952add3
commit 620f401091
5 changed files with 246 additions and 26 deletions
+165 -24
View File
@@ -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>
</div>
<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()">
<label class="form-check-label small text-muted" for="toggle-plumbing" title="Show containers, oauth clients, and sidecars">Plumbing</label>
<input class="form-check-input" type="checkbox" id="toggle-secrets-only" onchange="renderTable()">
<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>
<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;">
<option value="name">Name (A-Z)</option>
<option value="kind">Kind</option>
@@ -68,8 +68,8 @@
<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
</button>
<button class="btn btn-sm btn-primary ms-1 shadow-sm" onclick="openAddModal()">
<i class="fas fa-plus"></i> Add Resource
<button class="btn btn-sm btn-primary ms-1 shadow-sm" onclick="openAddSiteModal()">
<i class="fas fa-plus"></i> Add Site
</button>
</div>
</div>
@@ -98,6 +98,7 @@
<strong>{{name}}</strong>
</a>
<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-info me-1">Dev</span>{{/metadata.isProduction}}
</td>
@@ -206,9 +207,15 @@
</td>
<td class="text-end pe-3">
{{^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
</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}}
<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>`;
}
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">
<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>
</div>
@@ -856,18 +883,41 @@
<!-- Disks 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-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 class="card-body p-2">
${disksHtml}
</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="col-6">GPU <strong>${gpu}</strong></div>
<div class="col-6">ZFS <strong>${esc(t.zfs_health || 'N/A')}</strong></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 -->
<div class="card mb-3 border-danger shadow-sm">
<div class="card-header bg-danger text-white py-2">
@@ -1041,22 +1091,21 @@
function renderTable() {
const filter = $('#search-filter').val().toLowerCase();
const sort = $('#sort-by').val();
const showPlumbing = $('#toggle-plumbing').is(':checked');
const secretsOnly = $('#toggle-secrets-only').is(':checked');
let filtered = rawResources.filter(r => {
if (!showPlumbing && !filter) {
const sub = (r.metadata?.subType || '').toLowerCase();
if (r.kind === 'container' || r.kind === 'oauth' || sub === 'sidecar' || sub === 'container' || sub === 'openresty') {
return false;
}
if (secretsOnly && !r.hasSecret) {
return false;
}
if (!filter) return true;
const secretKeysStr = (r.secretKeys || []).join(' ').toLowerCase();
return (r.name || '').toLowerCase().includes(filter) ||
(r.slug || '').toLowerCase().includes(filter) ||
(r.kind || '').toLowerCase().includes(filter) ||
(r.metadata?.subType || '').toLowerCase().includes(filter) ||
(r.metadata?.ip || '').toLowerCase().includes(filter) ||
(r.hostName || '').toLowerCase().includes(filter);
(r.hostName || '').toLowerCase().includes(filter) ||
secretKeysStr.includes(filter);
});
// Sort
@@ -1352,6 +1401,96 @@
populateHostDropdown(parentId || '');
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;
async function loadLdapGroups() {
@@ -1753,22 +1892,24 @@
}
async function inheritParentSecret() {
const childKey = $('#inherit-child-key').val().trim();
let childKey = $('#inherit-child-key').val().trim();
const inheritVal = $('#inherit-parent-select').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) {
app.messages.action('Select a parent secret to inherit from.', $('#secrets-tab-container'), 'warning');
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;
try {