feat: integrate real administration retry and approved feature destinations
This commit is contained in:
1 parent
fef3cb35a8
commit
2be5725fbd
4 files changed
+32
-3
No files matched your search
@@ -54,6 +54,7 @@ COPY web/index.html /var/www/voice/
|
|||||||
COPY web/app.js /var/www/voice/
|
COPY web/app.js /var/www/voice/
|
||||||
COPY web/chat.js /var/www/voice/
|
COPY web/chat.js /var/www/voice/
|
||||||
COPY web/features.js /var/www/voice/
|
COPY web/features.js /var/www/voice/
|
||||||
|
COPY web/settings.js /var/www/voice/
|
||||||
COPY web/hope-themes.css /var/www/voice/
|
COPY web/hope-themes.css /var/www/voice/
|
||||||
COPY web/auth.js /var/www/voice/
|
COPY web/auth.js /var/www/voice/
|
||||||
COPY web/brand /var/www/voice/brand
|
COPY web/brand /var/www/voice/brand
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ def main():
|
|||||||
if page.locator(selector).count() and page.locator(selector).is_visible():
|
if page.locator(selector).count() and page.locator(selector).is_visible():
|
||||||
page.locator(selector).click()
|
page.locator(selector).click()
|
||||||
break
|
break
|
||||||
|
# Approved composer action starts directly. Legacy releases require
|
||||||
|
# a second Start click; never double-start a direct voice session.
|
||||||
|
direct = page.evaluate('!!window.HopeVoice')
|
||||||
|
if not direct:
|
||||||
expect(page.locator('#voiceModeBtn')).to_be_visible(timeout=10000)
|
expect(page.locator('#voiceModeBtn')).to_be_visible(timeout=10000)
|
||||||
page.locator('#voiceModeBtn').click()
|
page.locator('#voiceModeBtn').click()
|
||||||
expect(page.locator('#startBtn')).to_be_enabled(timeout=10000)
|
expect(page.locator('#startBtn')).to_be_enabled(timeout=10000)
|
||||||
|
|||||||
+14
-1
@@ -56,7 +56,20 @@
|
|||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.append(cell(`${account.display_name || account.username} (@${account.username})`), cell(account.role), cell(account.status), cell(account.container_name), cell(account.error));
|
row.append(cell(`${account.display_name || account.username} (@${account.username})`), cell(account.role), cell(account.status), cell(account.container_name), cell(account.error));
|
||||||
const actions = cell(''); actions.textContent = ''; actions.className = 'user-actions';
|
const actions = cell(''); actions.textContent = ''; actions.className = 'user-actions';
|
||||||
actions.append(action('Reset password', () => openDialog(account, 'password')), action('Delete', () => openDialog(account, 'delete'))); row.append(actions); el('usersBody').append(row);
|
if (account.status === 'error') {
|
||||||
|
let retrying = false;
|
||||||
|
const retry = action('Retry provisioning', async () => {
|
||||||
|
if (retrying || user?.role !== 'admin') return;
|
||||||
|
retrying = true; retry.disabled = true; const retryEpoch = epoch;
|
||||||
|
try { await request(`/api/admin/users/${encodeURIComponent(account.id)}/retry`, {method: 'POST'}); if (retryEpoch === epoch) await refreshUsers(); }
|
||||||
|
catch (error) { if (retryEpoch === epoch) say('adminMessage', error.message); }
|
||||||
|
finally { retrying = false; retry.disabled = false; }
|
||||||
|
});
|
||||||
|
actions.append(retry);
|
||||||
|
}
|
||||||
|
actions.append(action('Reset password', () => openDialog(account, 'password')), action('Delete', () => openDialog(account, 'delete')));
|
||||||
|
Array.from(row.children).forEach((td, index) => td.dataset.label = ['Account', 'Role', 'Status', 'Private assistant', 'Error'][index]);
|
||||||
|
actions.dataset.label = 'Actions'; row.append(actions); el('usersBody').append(row);
|
||||||
}
|
}
|
||||||
say('adminMessage', 'Account and container status refresh automatically.');
|
say('adminMessage', 'Account and container status refresh automatically.');
|
||||||
} catch (error) { say('adminMessage', error.message); } finally { refreshing = false; }
|
} catch (error) { say('adminMessage', error.message); } finally { refreshing = false; }
|
||||||
|
|||||||
+13
-2
@@ -132,11 +132,12 @@
|
|||||||
previews.setAttribute('aria-live', 'polite');
|
previews.setAttribute('aria-live', 'polite');
|
||||||
const input = node('input'); input.type = 'file'; input.multiple = true; input.hidden = true; input.setAttribute('aria-label', 'Choose attachments');
|
const input = node('input'); input.type = 'file'; input.multiple = true; input.hidden = true; input.setAttribute('aria-label', 'Choose attachments');
|
||||||
const attach = button('Attach files', () => input.click()); attach.dataset.attach = ''; input.addEventListener('change', () => {addFiles(input.files); input.value = '';}); toolbar.append(attach, input);
|
const attach = button('Attach files', () => input.click()); attach.dataset.attach = ''; input.addEventListener('change', () => {addFiles(input.files); input.value = '';}); toolbar.append(attach, input);
|
||||||
panel = node('section'); panel.id = 'hopeFeatures'; panel.setAttribute('aria-label', 'Saved work and assistant controls'); ($('chatSidebar') || document.body).append(panel);
|
panel = node('section'); panel.id = 'hopeFeatures'; panel.setAttribute('aria-label', 'Saved work and assistant controls'); ($('libraryContent') || $('chatSidebar') || document.body).append(panel);
|
||||||
status = node('p'); status.id = 'featureStatus'; status.setAttribute('role', 'status'); const availability = node('p'); availability.id = 'featureAvailability'; const availabilityDetails = node('details'); availabilityDetails.append(node('summary', 'Feature availability'), availability); panel.append(status, availabilityDetails);
|
status = node('p'); status.id = 'featureStatus'; status.setAttribute('role', 'status'); const availability = node('p'); availability.id = 'featureAvailability'; const availabilityDetails = node('details'); availabilityDetails.append(node('summary', 'Feature availability'), availability); panel.append(status, availabilityDetails);
|
||||||
filesSection.value = section('Files'); artifactSection.value = section('Artifacts'); artifactEditor = node('div'); artifactSection.value.detail.append(artifactEditor);
|
filesSection.value = section('Files'); artifactSection.value = section('Artifacts'); artifactEditor = node('div'); artifactSection.value.detail.append(artifactEditor);
|
||||||
taskSection.value = section('Scheduled tasks'); taskEditor = node('div'); taskSection.value.detail.append(taskEditor);
|
taskSection.value = section('Scheduled tasks'); taskEditor = node('div'); taskSection.value.detail.append(taskEditor);
|
||||||
memorySection.value = section('Memory'); memoryEditor = node('div'); memorySection.value.detail.append(memoryEditor);
|
memorySection.value = section('Memory'); memoryEditor = node('div'); memorySection.value.detail.append(memoryEditor);
|
||||||
|
if ($('activityContent')) $('activityContent').append(taskSection.value.detail);
|
||||||
for (const [area, load] of [[filesSection.value, loadFiles], [artifactSection.value, loadArtifacts], [taskSection.value, loadTasks], [memorySection.value, loadMemory]]) area.detail.addEventListener('toggle', () => {if (area.detail.open && state.user) load().catch(error => {if (error.name !== 'AbortError') message(error.message);});});
|
for (const [area, load] of [[filesSection.value, loadFiles], [artifactSection.value, loadArtifacts], [taskSection.value, loadTasks], [memorySection.value, loadMemory]]) area.detail.addEventListener('toggle', () => {if (area.detail.open && state.user) load().catch(error => {if (error.name !== 'AbortError') message(error.message);});});
|
||||||
const drop = options.dropTarget || $('primaryComposer') || toolbar;
|
const drop = options.dropTarget || $('primaryComposer') || toolbar;
|
||||||
drop.addEventListener('dragover', event => {if (event.dataTransfer?.types.includes('Files')) event.preventDefault();});
|
drop.addEventListener('dragover', event => {if (event.dataTransfer?.types.includes('Files')) event.preventDefault();});
|
||||||
@@ -147,6 +148,16 @@
|
|||||||
state.user = user; toolbar.hidden = panel.hidden = !user; if (user && (accountChanged || !Object.keys(state.capabilities).length)) refresh().catch(error => {if (error.name !== 'AbortError') message(error.message);});
|
state.user = user; toolbar.hidden = panel.hidden = !user; if (user && (accountChanged || !Object.keys(state.capabilities).length)) refresh().catch(error => {if (error.name !== 'AbortError') message(error.message);});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.HopeFeatures = {initialize, attachmentIDs, hasPendingUploads, clearAttachmentsAfterSend, clearAfterSend: clearAttachmentsAfterSend, addFiles, refresh};
|
function mountSection(name, target) {
|
||||||
|
const area = name === 'memory' ? memorySection.value : name === 'tasks' ? taskSection.value : name === 'files' ? filesSection.value : name === 'artifacts' ? artifactSection.value : null;
|
||||||
|
if (area && target) target.append(area.detail);
|
||||||
|
return area?.detail || null;
|
||||||
|
}
|
||||||
|
function openSection(name) {
|
||||||
|
const area = name === 'memory' ? memorySection.value : name === 'tasks' ? taskSection.value : name === 'files' ? filesSection.value : name === 'artifacts' ? artifactSection.value : null;
|
||||||
|
if (area) area.detail.open = true;
|
||||||
|
}
|
||||||
|
window.HopeFeatures = {initialize, attachmentIDs, hasPendingUploads, clearAttachmentsAfterSend, clearAfterSend: clearAttachmentsAfterSend, addFiles, refresh,
|
||||||
|
mountMemory: target => mountSection('memory', target), mountTasks: target => mountSection('tasks', target), openSection};
|
||||||
initialize();
|
initialize();
|
||||||
})();
|
})();
|
||||||
Reference in new issue
Block a user