feat: integrate real administration retry and approved feature destinations

This commit is contained in:
snowspeeder committed 2026-10-03 13:19:04 -04:00
1 parent fef3cb35a8
commit 2be5725fbd
4 files changed
+32 -3

No files matched your search

+1
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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();
})(); })();