feat: a recording survives switching to the Assistant, and signing out keeps it
Switching to the Assistant set window.location, which reloads the document
and silently ended any running recording. The assistant is a tab in the same
page and activateTab already rewrites the URL to /assistant, so the switch
now happens in place; the reload stays as a fallback. Verified in Chromium:
recorder still running, no reload, URL /assistant, assistant visible.
Signing out mid-recording used to end it with nothing kept. It now says so
first — "the audio will be saved for 24 hours so you can transcribe it
later" — and stores the audio either way, tagged with the module that
produced it ('encounter', 'soap', 'dictation'), which is what makes it
findable in Settings afterwards. Sign-out completes whether or not the save
worked, and the rescue never rejects, because the caller is on its way out.
Verified: the warning appears and the upload carries module=encounter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
8e4c70aaa8
commit
80d468a85d
4 changed files with 85 additions and 5 deletions
|
|
@ -184,8 +184,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
return;
|
||||
}
|
||||
if (!onAssistant) {
|
||||
// In the app you ARE the workspace, so only the Assistant pill navigates.
|
||||
if (wantsAssistant) window.location.href = '/assistant';
|
||||
// In the app you ARE the workspace, so only the Assistant pill moves.
|
||||
// This used to set window.location, which reloads the document and kills
|
||||
// a running recording. The assistant is a tab in this same page, and
|
||||
// activateTab already rewrites the URL to /assistant, so switching keeps
|
||||
// the recorder — and everything else — alive.
|
||||
if (!wantsAssistant) return;
|
||||
if (typeof window.activateTab === 'function') window.activateTab('assistant');
|
||||
else window.location.href = '/assistant';
|
||||
return;
|
||||
}
|
||||
// Inside the assistant, Workspace opens the launcher in place rather than
|
||||
|
|
@ -1015,6 +1021,28 @@ window.addEventListener('account-boundary', function() {
|
|||
if (_wakeLock) { try { _wakeLock.release(); } catch (e) {} _wakeLock = null; }
|
||||
});
|
||||
|
||||
// Every running recorder, so a logout or a navigation can find one and save it
|
||||
// rather than discarding minutes of a consultation.
|
||||
var _activeRecorders = new Set();
|
||||
window.activeRecordingCount = function() { return _activeRecorders.size; };
|
||||
|
||||
// Stops each running recording and stores it, tagged with the module it came
|
||||
// from, so it can be picked up and transcribed later. Resolves once every one
|
||||
// is stored (or has failed to store) — never rejects, because the caller is
|
||||
// usually on its way out of the app.
|
||||
window.rescueActiveRecordings = function() {
|
||||
var recorders = Array.from(_activeRecorders);
|
||||
if (!recorders.length) return Promise.resolve([]);
|
||||
return Promise.all(recorders.map(function(recorder) {
|
||||
var module = recorder._module || 'recording';
|
||||
return recorder.stop().then(function(blob) {
|
||||
if (!blob || !blob.size || typeof saveAudioBackup !== 'function') return null;
|
||||
return saveAudioBackup(blob, module);
|
||||
}).then(function(id) { return { module: module, id: id }; })
|
||||
.catch(function() { return { module: module, id: null }; });
|
||||
}));
|
||||
};
|
||||
|
||||
function AudioRecorder() { this.mediaRecorder = null; this.chunks = []; this.stream = null; }
|
||||
AudioRecorder.prototype.start = function() {
|
||||
var self = this;
|
||||
|
|
@ -1052,6 +1080,7 @@ AudioRecorder.prototype.start = function() {
|
|||
});
|
||||
});
|
||||
self.mediaRecorder.start(1000);
|
||||
_activeRecorders.add(self);
|
||||
self.heldWakeLock = true;
|
||||
holdWakeLock();
|
||||
});
|
||||
|
|
@ -1072,6 +1101,7 @@ AudioRecorder.prototype.notifyFailure = function() {
|
|||
|
||||
AudioRecorder.prototype.stop = function() {
|
||||
var self = this;
|
||||
_activeRecorders.delete(self);
|
||||
if (self.heldWakeLock) { self.heldWakeLock = false; releaseWakeLock(); }
|
||||
return new Promise(function(resolve) {
|
||||
if (!self.mediaRecorder || self.mediaRecorder.state === 'inactive') { resolve(null); return; }
|
||||
|
|
|
|||
|
|
@ -563,8 +563,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// Logout button
|
||||
if (target.id === 'btn-logout' || target.closest('#btn-logout')) {
|
||||
e.preventDefault();
|
||||
exitApp();
|
||||
showToast('Logged out', 'info');
|
||||
// Signing out mid-recording used to end it with nothing kept. Say so, and
|
||||
// store the audio either way, tagged with the encounter it came from, so
|
||||
// it can be transcribed from Settings afterwards.
|
||||
var recording = typeof activeRecordingCount === 'function' && activeRecordingCount() > 0;
|
||||
if (!recording) { exitApp(); showToast('Logged out', 'info'); return; }
|
||||
showConfirm('A recording is still running. Signing out stops it — the audio will be saved for 24 hours so you can transcribe it later. Sign out?', function() {
|
||||
showToast('Saving the recording...', 'info');
|
||||
rescueActiveRecordings().then(function(saved) {
|
||||
var kept = saved.filter(function(entry) { return entry && entry.id; });
|
||||
if (kept.length) showToast('Recording saved (' + kept[0].module + '). Find it in Settings → Audio backups.', 'success');
|
||||
else showToast('The recording could not be saved', 'error');
|
||||
}).finally(function() {
|
||||
exitApp();
|
||||
showToast('Logged out', 'info');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// About modal
|
||||
|
|
|
|||
|
|
@ -253,7 +253,11 @@ test('the switch navigates from the app and opens the launcher inside the assist
|
|||
const app = read('public/js/app.js');
|
||||
const handler = app.slice(app.indexOf('// Assistant / Workspace switch'), app.indexOf('window.activateTab = activateTab;'));
|
||||
// In the app you ARE the workspace, so only the Assistant pill goes anywhere.
|
||||
assert.match(handler, /if \(wantsAssistant\) window\.location\.href = '\/assistant';/);
|
||||
// It switches in place rather than setting window.location, which reloads the
|
||||
// document and would end a running recording.
|
||||
assert.match(handler, /if \(!wantsAssistant\) return;/);
|
||||
assert.match(handler, /if \(typeof window\.activateTab === 'function'\) window\.activateTab\('assistant'\);/);
|
||||
assert.match(handler, /else window\.location\.href = '\/assistant';/);
|
||||
assert.match(handler, /assistantShowWorkspaceLauncher\(!wantsAssistant\)/, 'in the assistant it swaps the view in place');
|
||||
|
||||
// The in-page mode machinery is gone, and with it the whole class of
|
||||
|
|
|
|||
|
|
@ -172,3 +172,35 @@ test('every recording is kept for 24 hours, not only the failures', () => {
|
|||
assert.match(db, /ALTER TABLE audio_backups ADD COLUMN IF NOT EXISTS storage_key TEXT;/,
|
||||
'existing installations get the column too');
|
||||
});
|
||||
|
||||
test('signing out mid-recording warns, and keeps the audio with its encounter', () => {
|
||||
const app = read('public/js/app.js');
|
||||
const auth = read('public/js/auth.js');
|
||||
// Every running recorder is registered, so anything about to end the session
|
||||
// can find one instead of discarding minutes of a consultation.
|
||||
assert.match(app, /var _activeRecorders = new Set\(\);/);
|
||||
assert.match(app, /_activeRecorders\.add\(self\);/);
|
||||
assert.match(app, /_activeRecorders\.delete\(self\);/);
|
||||
assert.match(app, /window\.rescueActiveRecordings = function\(\)/);
|
||||
// Tagged with the module that produced it, which is what makes it findable
|
||||
// afterwards: 'encounter', 'soap', 'dictation'.
|
||||
assert.match(app, /var module = recorder\._module \|\| 'recording';/);
|
||||
assert.match(app, /return saveAudioBackup\(blob, module\);/);
|
||||
// The caller is on its way out of the app, so this must never reject.
|
||||
assert.match(app, /\.catch\(function\(\) \{ return \{ module: module, id: null \}; \}\);/);
|
||||
|
||||
assert.match(auth, /activeRecordingCount\(\) > 0/);
|
||||
assert.match(auth, /showConfirm\('A recording is still running\./);
|
||||
assert.match(auth, /rescueActiveRecordings\(\)\.then/);
|
||||
assert.match(auth, /\.finally\(function\(\) \{\s*\n\s*exitApp\(\);/, 'sign-out completes whether or not the save worked');
|
||||
});
|
||||
|
||||
test('switching to the Assistant does not reload the page out from under a recording', () => {
|
||||
const app = read('public/js/app.js');
|
||||
const handler = app.slice(app.indexOf('if (!onAssistant) {'), app.indexOf('// Inside the assistant, Workspace opens the launcher'));
|
||||
// window.location reloads the document, which ends any running recording.
|
||||
assert.match(handler, /if \(typeof window\.activateTab === 'function'\) window\.activateTab\('assistant'\);/);
|
||||
assert.match(handler, /else window\.location\.href = '\/assistant';/, 'the reload stays as a fallback');
|
||||
// activateTab already rewrites the URL, so the address still reads /assistant.
|
||||
assert.match(app, /var target = tabName === 'assistant' \? '\/assistant' : '\/';/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue