Auth-before-mic connect flow and generic deploy docs
This commit is contained in:
@@ -46,6 +46,7 @@
|
||||
</section>
|
||||
<div class="controls" id="button-container">
|
||||
<button id="connectButton">Connect</button>
|
||||
<span id="authModeSeparator" aria-hidden="true">or</span>
|
||||
<button id="showRegisterButton" type="button">Register</button>
|
||||
<button id="logoutButton" class="hidden">Log out</button>
|
||||
<button id="settingsButton">Audio setup</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Maintainer-controlled web client version.
|
||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||
window.CHGRID_WEB_VERSION = "2026.02.25 R255";
|
||||
window.CHGRID_WEB_VERSION = "2026.02.25 R256";
|
||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||
|
||||
@@ -102,6 +102,7 @@ type Dom = {
|
||||
authPolicyHintRegister: HTMLParagraphElement;
|
||||
authSessionView: HTMLElement;
|
||||
authSessionText: HTMLParagraphElement;
|
||||
authModeSeparator: HTMLElement;
|
||||
showRegisterButton: HTMLButtonElement;
|
||||
updatesSection: HTMLElement;
|
||||
updatesToggle: HTMLButtonElement;
|
||||
@@ -136,6 +137,7 @@ const dom: Dom = {
|
||||
authPolicyHintRegister: requiredById('authPolicyHintRegister'),
|
||||
authSessionView: requiredById('authSessionView'),
|
||||
authSessionText: requiredById('authSessionText'),
|
||||
authModeSeparator: requiredById('authModeSeparator'),
|
||||
showRegisterButton: requiredById('showRegisterButton'),
|
||||
updatesSection: requiredById('updatesSection'),
|
||||
updatesToggle: requiredById('updatesToggle'),
|
||||
@@ -599,11 +601,13 @@ function updateConnectAvailability(): void {
|
||||
const label = sanitizeAuthUsername(authUsername) || 'current account';
|
||||
dom.authSessionText.textContent = `Logged in as ${label}.`;
|
||||
dom.showRegisterButton.classList.add('hidden');
|
||||
dom.authModeSeparator.classList.add('hidden');
|
||||
dom.loginView.classList.add('hidden');
|
||||
dom.registerView.classList.add('hidden');
|
||||
dom.authSessionView.classList.remove('hidden');
|
||||
} else {
|
||||
dom.showRegisterButton.classList.remove('hidden');
|
||||
dom.authModeSeparator.classList.remove('hidden');
|
||||
dom.showRegisterButton.textContent = authMode === 'login' ? 'Register' : 'Login';
|
||||
dom.loginView.classList.toggle('hidden', authMode !== 'login');
|
||||
dom.registerView.classList.toggle('hidden', authMode !== 'register');
|
||||
@@ -1525,11 +1529,6 @@ function getConnectionFlowDeps(): ConnectFlowDeps {
|
||||
updateConnectAvailability,
|
||||
mediaIsConnecting: () => mediaSession.isConnecting(),
|
||||
mediaSetConnecting: (value) => mediaSession.setConnecting(value),
|
||||
mediaCheckMicPermission: () => checkMicPermission(),
|
||||
mediaPopulateAudioDevices: () => populateAudioDevices(),
|
||||
mediaGetPreferredInputDeviceId: () => mediaSession.getPreferredInputDeviceId(),
|
||||
mediaSetupLocalMedia: (audioDeviceId) => setupLocalMedia(audioDeviceId),
|
||||
mediaDescribeError: (error) => describeMediaError(error),
|
||||
mediaStopLocalMedia: () => stopLocalMedia(),
|
||||
signalingConnect: (handler) => signaling.connect(handler as (message: IncomingMessage) => Promise<void>),
|
||||
signalingSendAuth: () => sendAuthRequest(),
|
||||
@@ -1671,6 +1670,9 @@ async function onSignalingMessage(message: IncomingMessage): Promise<void> {
|
||||
startHeartbeat();
|
||||
}
|
||||
await onAppMessage(message);
|
||||
if (message.type === 'welcome') {
|
||||
void setupMediaAfterAuth();
|
||||
}
|
||||
itemBehaviorRegistry.onUseResultMessage(message);
|
||||
itemBehaviorRegistry.onWorldUpdate();
|
||||
applyConfiguredPeerListenGains();
|
||||
@@ -1685,6 +1687,28 @@ async function onSignalingMessage(message: IncomingMessage): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Requests microphone access and initializes local media after successful auth/welcome. */
|
||||
async function setupMediaAfterAuth(): Promise<void> {
|
||||
if (!state.running) return;
|
||||
const canProceed = await checkMicPermission();
|
||||
if (!canProceed) {
|
||||
setConnectionStatus('Microphone access is required.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await populateAudioDevices();
|
||||
if (dom.audioInputSelect.options.length === 0) {
|
||||
setConnectionStatus('No audio input device found. Open Audio setup or connect a microphone.');
|
||||
return;
|
||||
}
|
||||
const inputDeviceId = dom.audioInputSelect.value || mediaSession.getPreferredInputDeviceId();
|
||||
await setupLocalMedia(inputDeviceId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setConnectionStatus(describeMediaError(error));
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles local microphone track mute state. */
|
||||
function toggleMute(): void {
|
||||
state.isMuted = !state.isMuted;
|
||||
|
||||
@@ -19,11 +19,6 @@ export type ConnectFlowDeps = {
|
||||
updateConnectAvailability: () => void;
|
||||
mediaIsConnecting: () => boolean;
|
||||
mediaSetConnecting: (value: boolean) => void;
|
||||
mediaCheckMicPermission: () => Promise<boolean>;
|
||||
mediaPopulateAudioDevices: () => Promise<void>;
|
||||
mediaGetPreferredInputDeviceId: () => string;
|
||||
mediaSetupLocalMedia: (audioDeviceId: string) => Promise<void>;
|
||||
mediaDescribeError: (error: unknown) => string;
|
||||
mediaStopLocalMedia: () => void;
|
||||
signalingConnect: (onMessage: (message: unknown) => Promise<void>) => Promise<void>;
|
||||
signalingSendAuth: () => void;
|
||||
@@ -36,7 +31,7 @@ export type ConnectFlowDeps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs connect flow: preflight media setup, then signaling connect/auth.
|
||||
* Runs connect flow: signaling connect/auth first, media setup after auth/welcome.
|
||||
*/
|
||||
export async function runConnectFlow(deps: ConnectFlowDeps): Promise<void> {
|
||||
if (deps.mediaIsConnecting() || deps.state.running) {
|
||||
@@ -47,32 +42,6 @@ export async function runConnectFlow(deps: ConnectFlowDeps): Promise<void> {
|
||||
deps.mediaSetConnecting(true);
|
||||
deps.updateConnectAvailability();
|
||||
|
||||
const canProceed = await deps.mediaCheckMicPermission();
|
||||
if (!canProceed) {
|
||||
deps.updateStatus('Microphone access is required.');
|
||||
deps.mediaSetConnecting(false);
|
||||
deps.updateConnectAvailability();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deps.mediaPopulateAudioDevices();
|
||||
if (deps.dom.audioInputSelect.options.length === 0) {
|
||||
deps.updateStatus('No audio input device found. Open Settings or connect a microphone.');
|
||||
deps.mediaSetConnecting(false);
|
||||
deps.updateConnectAvailability();
|
||||
return;
|
||||
}
|
||||
const inputDeviceId = deps.dom.audioInputSelect.value || deps.mediaGetPreferredInputDeviceId();
|
||||
await deps.mediaSetupLocalMedia(inputDeviceId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
deps.updateStatus(deps.mediaDescribeError(error));
|
||||
deps.mediaSetConnecting(false);
|
||||
deps.updateConnectAvailability();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deps.signalingConnect(deps.onMessage);
|
||||
deps.signalingSendAuth();
|
||||
|
||||
@@ -77,6 +77,11 @@ body {
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#authModeSeparator {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
|
||||
Reference in New Issue
Block a user