fix: a failed recording keeps its tab and the retry adds the words after the last word; Settings drops the session list; FAQ says what sign out does
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
cba2a312e1
commit
9c86c56968
7 changed files with 26 additions and 89 deletions
|
|
@ -211,17 +211,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<button class="faq-question">How do I manage my active sessions?</button>
|
||||
<div class="faq-answer">
|
||||
<p>Go to <strong>Settings > Active Sessions</strong> to see all devices where you are logged in. You can revoke any session individually or click <strong>Revoke All Other Sessions</strong> to log out every other device. Your current session is highlighted and cannot be revoked from this screen — use the Logout button instead.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<button class="faq-question">How do I sign out everywhere?</button>
|
||||
<div class="faq-answer">
|
||||
<p><strong>Settings > Active Sessions > Revoke All Other Sessions</strong> signs out every other device from this app. Your PedsHub sign-in itself can be ended from the PedsHub sign-in page's account menu, which signs you out of every PedsHub app at once.</p>
|
||||
<p>Press <strong>Sign out</strong>. It ends your session in this app and your PedsHub sign-in itself, so the quiz app is signed out too and the next person at the computer cannot get back in with one click. Signing in again is one press of <strong>Sign in with PedsHub</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -106,18 +106,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Sessions — hidden for SSO-only users (revoke can't stick against an active IdP session) -->
|
||||
<div class="settings-section card" id="sessions-section" style="display:none;">
|
||||
<h3><i class="fas fa-desktop"></i> Active Sessions</h3>
|
||||
<p style="font-size:13px;color:var(--g600);">Devices where you are currently logged in. Revoke any session to immediately log that device out.</p>
|
||||
<div style="margin-bottom:10px;">
|
||||
<button id="btn-revoke-all-sessions" class="btn-sm btn-ghost" style="color:var(--red);font-size:12px;"><i class="fas fa-ban"></i> Revoke All Other Sessions</button>
|
||||
</div>
|
||||
<div id="sessions-list" style="display:flex;flex-direction:column;gap:6px;">
|
||||
<p style="color:var(--g400);font-size:13px;">Loading sessions...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nextcloud -->
|
||||
<div class="settings-section card" data-feature="nextcloud">
|
||||
<h3><i class="fas fa-cloud"></i> Nextcloud Integration</h3>
|
||||
|
|
|
|||
|
|
@ -567,7 +567,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
document.addEventListener('tabChanged', function(e) {
|
||||
if (e.detail && e.detail.tab === 'settings') {
|
||||
if (typeof load2FAStatus === 'function') load2FAStatus();
|
||||
if (typeof loadSessions === 'function') loadSessions();
|
||||
if (typeof loadNextcloudStatus === 'function') loadNextcloudStatus();
|
||||
if (typeof loadMemories === 'function') loadMemories();
|
||||
if (typeof loadSavedEncountersList === 'function') loadSavedEncountersList();
|
||||
|
|
@ -1288,7 +1287,7 @@ function _serverTranscribe(blob, module) {
|
|||
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (!data.success && blob.size > 0 && typeof saveAudioBackup === 'function') {
|
||||
console.log('[AudioBackup] Transcription failed, saving backup...');
|
||||
saveAudioBackup(blob, 'failed-transcription').then(function(id) {
|
||||
saveAudioBackup(blob, moduleName || 'failed-transcription').then(function(id) {
|
||||
console.log('[AudioBackup] Saved with id:', id);
|
||||
showToast('Audio backed up for retry', 'info');
|
||||
}).catch(function(e) { console.error('[AudioBackup] Save failed:', e); });
|
||||
|
|
@ -1297,7 +1296,7 @@ function _serverTranscribe(blob, module) {
|
|||
}).catch(function(err) {
|
||||
if (blob.size > 0 && typeof saveAudioBackup === 'function') {
|
||||
console.log('[AudioBackup] Transcription error, saving backup...');
|
||||
saveAudioBackup(blob, 'failed-transcription').then(function(id) {
|
||||
saveAudioBackup(blob, moduleName || 'failed-transcription').then(function(id) {
|
||||
console.log('[AudioBackup] Saved with id:', id);
|
||||
showToast('Audio backed up for retry', 'info');
|
||||
}).catch(function(e) { console.error('[AudioBackup] Save failed:', e); });
|
||||
|
|
|
|||
|
|
@ -275,10 +275,17 @@ function guardTransaction(tx, owner) {
|
|||
}
|
||||
return setTimeout(place, 50);
|
||||
}
|
||||
var existing = (box.textContent || '').trim();
|
||||
box.textContent = existing ? existing + '\n\n' + text : text;
|
||||
// After the last word, as dictation would have continued: one space,
|
||||
// no blank lines, and the caret left at the end of what was added.
|
||||
var existing = (box.textContent || '').replace(/\s+$/, '');
|
||||
box.textContent = existing ? existing + ' ' + text.trim() : text.trim();
|
||||
box.scrollIntoView({ block: 'center' });
|
||||
try { box.focus(); } catch (e) {}
|
||||
try {
|
||||
box.focus();
|
||||
var range = document.createRange(); range.selectNodeContents(box); range.collapse(false);
|
||||
var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
||||
} catch (e) {}
|
||||
try { box.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
|
||||
showToast('Transcript added to ' + entry.label, 'success');
|
||||
}());
|
||||
return true;
|
||||
|
|
@ -436,7 +443,7 @@ function guardTransaction(tx, owner) {
|
|||
retryIcon.className = 'fas fa-rotate-right';
|
||||
retryBtn.appendChild(retryIcon);
|
||||
var retryTarget = window.RecordingModules && window.RecordingModules.lookup(b.module);
|
||||
retryBtn.appendChild(document.createTextNode(retryTarget ? ' Retry into ' + retryTarget.label : ' Retry'));
|
||||
retryBtn.appendChild(document.createTextNode(retryTarget ? ' Transcribe into ' + retryTarget.label : ' Transcribe'));
|
||||
retryBtn.title = retryTarget
|
||||
? 'Transcribe again and add the text to ' + retryTarget.label
|
||||
: 'Transcribe again and copy the text to the clipboard';
|
||||
|
|
|
|||
|
|
@ -1072,63 +1072,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
function escH(str) { var d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; }
|
||||
|
||||
window.loadSessions = function() {
|
||||
var list = document.getElementById('sessions-list');
|
||||
if (!list) return;
|
||||
fetch('/api/sessions', { headers: getAuthHeaders(), credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.success || !data.sessions || data.sessions.length === 0) {
|
||||
list.innerHTML = '<p style="color:var(--g400);font-size:13px;">No active sessions found.</p>';
|
||||
return;
|
||||
}
|
||||
var currentSid = data.currentSessionId || (window.SecureStorage ? window.SecureStorage.getSync(SESSION_KEY) : localStorage.getItem(SESSION_KEY));
|
||||
list.innerHTML = data.sessions.map(function(s) {
|
||||
var isCurrent = s.id === currentSid;
|
||||
var created = new Date(s.created_at).toLocaleDateString();
|
||||
var lastAct = timeAgo(new Date(s.last_activity));
|
||||
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--g50);border-radius:8px;border:1.5px solid ' + (isCurrent ? 'var(--blue)' : 'var(--g200)') + ';">'
|
||||
+ '<div>'
|
||||
+ '<div style="font-size:13px;font-weight:600;color:var(--g800);">' + escH(s.device_label || 'Unknown device') + (isCurrent ? ' <span style="font-size:11px;color:var(--blue);font-weight:500;">(this device)</span>' : '') + '</div>'
|
||||
+ '<div style="font-size:12px;color:var(--g500);margin-top:2px;">' + escH(s.ip_address || '') + ' · Created ' + created + ' · Active ' + lastAct + '</div>'
|
||||
+ '</div>'
|
||||
+ (isCurrent ? '' : '<button class="btn-sm btn-ghost session-revoke-btn" data-sid="' + s.id + '" style="color:var(--red);font-size:12px;"><i class="fas fa-xmark"></i> Revoke</button>')
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
|
||||
// Wire revoke buttons
|
||||
list.querySelectorAll('.session-revoke-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
showConfirm('Revoke this session? That device will be logged out.', function() {
|
||||
fetch('/api/sessions/' + btn.dataset.sid, { method: 'DELETE', headers: getAuthHeaders() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success) { showToast('Session revoked', 'info'); loadSessions(); }
|
||||
else showToast(d.error || 'Failed', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
list.innerHTML = '<p style="color:var(--g400);font-size:13px;">Could not load sessions.</p>';
|
||||
});
|
||||
};
|
||||
|
||||
// Revoke all other sessions button
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.id === 'btn-revoke-all-sessions' || e.target.closest('#btn-revoke-all-sessions')) {
|
||||
e.preventDefault();
|
||||
showConfirm('Revoke all other sessions? All other devices will be logged out.', function() {
|
||||
fetch('/api/sessions', { method: 'DELETE', headers: getAuthHeaders() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success) { showToast('All other sessions revoked (' + (d.revoked || 0) + ' removed)', 'info'); loadSessions(); }
|
||||
});
|
||||
}, { danger: true, confirmText: 'Revoke All' });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- CHANGE PASSWORD ----
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.id === 'btn-change-password' || e.target.closest('#btn-change-password')) {
|
||||
|
|
@ -1157,7 +1100,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (data.passwordWarning) setTimeout(function() { showToast(data.passwordWarning, 'warning'); }, 1500);
|
||||
current.value = ''; newPw.value = ''; confirmPw.value = '';
|
||||
if (status) { status.textContent = ''; }
|
||||
if (typeof loadSessions === 'function') loadSessions();
|
||||
} else {
|
||||
showToast(data.error || 'Failed', 'error');
|
||||
if (status) { status.textContent = ''; }
|
||||
|
|
@ -1190,13 +1132,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// legit local user's sections due to a transient fetch hiccup.
|
||||
var pwSection = document.getElementById('change-password-section');
|
||||
var twofaSection = document.getElementById('2fa-section');
|
||||
var sessionsSection = document.getElementById('sessions-section');
|
||||
if (data.user.canLocalAuth === false) {
|
||||
return; // SSO-only — keep sections hidden
|
||||
}
|
||||
if (pwSection) pwSection.style.display = '';
|
||||
if (twofaSection) twofaSection.style.display = '';
|
||||
if (sessionsSection) sessionsSection.style.display = '';
|
||||
var status = document.getElementById('2fa-status');
|
||||
var setupBtn = document.getElementById('btn-setup-2fa');
|
||||
var disableBtn = document.getElementById('btn-disable-2fa');
|
||||
|
|
|
|||
|
|
@ -314,3 +314,13 @@ test('sign-out ends the provider session: the server hands back the end-session
|
|||
assert.match(js, /if \(!leaving\) boundary\.reload\(\)/);
|
||||
assert.match(read('docs/authentication.md'), /## Signing out signs you out of PedsHub/);
|
||||
});
|
||||
|
||||
test('a failed transcription keeps its tab, the retry puts the words after the last word, and Settings has no session list', () => {
|
||||
const app = read('public/js/app.js');
|
||||
assert.equal((app.match(/saveAudioBackup\(blob, moduleName \|\| 'failed-transcription'\)/g) || []).length, 2);
|
||||
const backup = read('public/js/audioBackup.js');
|
||||
assert.match(backup, /existing \+ ' ' \+ text\.trim\(\)/);
|
||||
assert.match(backup, /' Transcribe into ' \+ retryTarget\.label/);
|
||||
assert.doesNotMatch(read('public/components/settings.html'), /sessions-section|btn-revoke-all-sessions/);
|
||||
assert.doesNotMatch(read('public/js/auth.js'), /loadSessions/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -281,8 +281,8 @@ test('a retried transcript goes back to the tab the audio came from', () => {
|
|||
assert.match(backup, /if \(!window\.activateTab\(entry\.tab\)\) return false;/, 'it opens that tab');
|
||||
// A retry usually recovers text ON TOP of a live transcript, so replacing the
|
||||
// box would lose the words the browser did hear.
|
||||
assert.match(backup, /box\.textContent = existing \? existing \+ '\\n\\n' \+ text : text;/,
|
||||
'existing text is appended to, never replaced');
|
||||
assert.match(backup, /box\.textContent = existing \? existing \+ ' ' \+ text\.trim\(\) : text\.trim\(\);/,
|
||||
'existing text is appended to after its last word, never replaced');
|
||||
// The box only exists once the tab's markup has been fetched.
|
||||
assert.match(backup, /if \(attempts\+\+ > 40\)/, 'it waits for the component rather than guessing a delay');
|
||||
assert.match(backup, /showToast\('Transcript copied — could not open '/, 'and falls back to the clipboard');
|
||||
|
|
|
|||
Loading…
Reference in a new issue