- {/* Record Button with Mode Selector - show when Chronicle connected OR desktop-mic is wired */}
- {(isChronicleConnected || isDesktopMicWired) && (
-
- {/* Mode Toggle - only show when not recording */}
- {!recording.isRecording && !isRecordingProcessing && (
-
-
-
-
+ {/* Chronicle Record Button - only show when connected */}
+ {isChronicleConnected && (
+
+
+ {recording.isRecording
+ ? recording.formatDuration(recording.recordingDuration)
+ : isRecordingProcessing
+ ? 'Starting...'
+ : 'Record'}
+
+
)}
{/* Test Feature Flag Indicator */}
diff --git a/ushadow/mobile/app/(tabs)/index.tsx b/ushadow/mobile/app/(tabs)/index.tsx
index 3d72fd6c..433320e8 100644
--- a/ushadow/mobile/app/(tabs)/index.tsx
+++ b/ushadow/mobile/app/(tabs)/index.tsx
@@ -195,7 +195,6 @@ export default function HomeScreen() {
visible={showLoginScreen}
onClose={() => setShowLoginScreen(false)}
onLoginSuccess={handleLoginSuccess}
- initialApiUrl="https://blue.spangled-kettle.ts.net"
/>
{/* Connection Log Viewer Modal */}
diff --git a/ushadow/mobile/app/_utils/authStorage.ts b/ushadow/mobile/app/_utils/authStorage.ts
index ee21cb23..d69ee18d 100644
--- a/ushadow/mobile/app/_utils/authStorage.ts
+++ b/ushadow/mobile/app/_utils/authStorage.ts
@@ -7,9 +7,11 @@
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
+import AppConfig from '../config';
const AUTH_TOKEN_KEY = '@ushadow_auth_token';
const API_URL_KEY = '@ushadow_api_url';
+const DEFAULT_SERVER_URL_KEY = '@ushadow_default_server_url';
/**
* Store the auth token
@@ -138,3 +140,60 @@ export function appendTokenToUrl(wsUrl: string, token: string): string {
const separator = wsUrl.includes('?') ? '&' : '?';
return `${wsUrl}${separator}token=${token}`;
}
+
+/**
+ * Get the default server URL.
+ * Returns user-configured default if set, otherwise returns app config default.
+ */
+export async function getDefaultServerUrl(): Promise {
+ try {
+ const customDefault = await AsyncStorage.getItem(DEFAULT_SERVER_URL_KEY);
+ if (customDefault) {
+ return customDefault;
+ }
+ } catch (error) {
+ console.error('[AuthStorage] Failed to get default server URL:', error);
+ }
+ return AppConfig.DEFAULT_SERVER_URL;
+}
+
+/**
+ * Set a custom default server URL.
+ * This will be used instead of the app config default.
+ */
+export async function setDefaultServerUrl(url: string): Promise {
+ try {
+ await AsyncStorage.setItem(DEFAULT_SERVER_URL_KEY, url);
+ console.log('[AuthStorage] Default server URL saved:', url);
+ } catch (error) {
+ console.error('[AuthStorage] Failed to save default server URL:', error);
+ throw error;
+ }
+}
+
+/**
+ * Clear the custom default server URL (revert to app config default).
+ */
+export async function clearDefaultServerUrl(): Promise {
+ try {
+ await AsyncStorage.removeItem(DEFAULT_SERVER_URL_KEY);
+ console.log('[AuthStorage] Default server URL cleared');
+ } catch (error) {
+ console.error('[AuthStorage] Failed to clear default server URL:', error);
+ throw error;
+ }
+}
+
+/**
+ * Get the effective server URL to use.
+ * Priority: stored API URL > custom default > app config default
+ */
+export async function getEffectiveServerUrl(): Promise {
+ // First check if there's a stored API URL from login
+ const storedUrl = await getApiUrl();
+ if (storedUrl) {
+ return storedUrl;
+ }
+ // Otherwise return the default
+ return getDefaultServerUrl();
+}
diff --git a/ushadow/mobile/app/components/LoginScreen.tsx b/ushadow/mobile/app/components/LoginScreen.tsx
index 4e98515d..3ebaccc3 100644
--- a/ushadow/mobile/app/components/LoginScreen.tsx
+++ b/ushadow/mobile/app/components/LoginScreen.tsx
@@ -11,7 +11,7 @@
* - Returns JWT token valid for both ushadow and Chronicle services
*/
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import {
View,
Text,
@@ -42,11 +42,21 @@ export const LoginScreen: React.FC = ({
}) => {
// Server URL format: https://{tailscale-host}
// Login will POST to {serverUrl}/api/auth/login
- const [apiUrl, setApiUrl] = useState(initialApiUrl || 'https://blue.spangled-kettle.ts.net');
+ const [apiUrl, setApiUrl] = useState(initialApiUrl || '');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+ const [saveAsDefault, setSaveAsDefault] = useState(false);
+
+ // Load the default server URL when modal opens
+ useEffect(() => {
+ if (visible && !initialApiUrl) {
+ getDefaultServerUrl().then((defaultUrl) => {
+ setApiUrl(defaultUrl);
+ });
+ }
+ }, [visible, initialApiUrl]);
const handleLogin = async () => {
if (!apiUrl.trim() || !email.trim() || !password.trim()) {
@@ -102,9 +112,16 @@ export const LoginScreen: React.FC = ({
await saveAuthToken(token);
await saveApiUrl(baseUrl);
+ // Optionally save as default server URL
+ if (saveAsDefault) {
+ await setDefaultServerUrl(baseUrl);
+ console.log('[Login] Saved as default server URL');
+ }
+
// Clear form
setEmail('');
setPassword('');
+ setSaveAsDefault(false);
// Notify parent
onLoginSuccess(token, baseUrl);
@@ -162,6 +179,17 @@ export const LoginScreen: React.FC = ({
keyboardType="url"
testID="login-api-url"
/>
+ {/* Save as default checkbox */}
+ setSaveAsDefault(!saveAsDefault)}
+ testID="login-save-default"
+ >
+
+ {saveAsDefault && ✓}
+
+ Save as default server
+
{/* Email */}
@@ -291,6 +319,34 @@ const styles = StyleSheet.create({
color: theme.textPrimary,
fontSize: fontSize.base,
},
+ checkboxRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: spacing.sm,
+ },
+ checkbox: {
+ width: 20,
+ height: 20,
+ borderRadius: 4,
+ borderWidth: 2,
+ borderColor: theme.textMuted,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: spacing.sm,
+ },
+ checkboxChecked: {
+ backgroundColor: theme.primaryButton,
+ borderColor: theme.primaryButton,
+ },
+ checkmark: {
+ color: theme.primaryButtonText,
+ fontSize: 12,
+ fontWeight: 'bold',
+ },
+ checkboxLabel: {
+ fontSize: fontSize.sm,
+ color: theme.textSecondary,
+ },
errorContainer: {
backgroundColor: colors.error.bgSolid,
padding: spacing.md,
diff --git a/ushadow/mobile/app/config.ts b/ushadow/mobile/app/config.ts
new file mode 100644
index 00000000..d668c4f8
--- /dev/null
+++ b/ushadow/mobile/app/config.ts
@@ -0,0 +1,25 @@
+/**
+ * App Configuration
+ *
+ * Central configuration for the Ushadow mobile app.
+ * The default server URL can be changed here or overridden by the user during setup.
+ */
+
+export const AppConfig = {
+ /**
+ * Default server URL.
+ * This is used as the initial value when the app is first installed.
+ * Users can change this in the login screen.
+ *
+ * Format: https://{your-tailscale-host}
+ * Example: https://blue.spangled-kettle.ts.net
+ */
+ DEFAULT_SERVER_URL: 'https://ushadow.wolf-tawny.ts.net',
+
+ /**
+ * App version info
+ */
+ APP_NAME: 'Ushadow Mobile',
+};
+
+export default AppConfig;
diff --git a/ushadow/mobile/app/services/chronicleApi.ts b/ushadow/mobile/app/services/chronicleApi.ts
index 3c3e73f3..688cb918 100644
--- a/ushadow/mobile/app/services/chronicleApi.ts
+++ b/ushadow/mobile/app/services/chronicleApi.ts
@@ -101,10 +101,11 @@ async function getChronicleApiUrl(): Promise {
return chronicleUrl;
}
- // Default fallback
- console.log('[ChronicleAPI] Using default Chronicle generic proxy');
- return 'https://blue.spangled-kettle.ts.net/api/services/chronicle-backend/proxy';
-}
+ // Default fallback - use configured default server URL
+ const defaultUrl = await getDefaultServerUrl();
+ console.log(`[ChronicleAPI] Using default Chronicle generic proxy: ${defaultUrl}`);
+ return `${defaultUrl}/api/services/chronicle-backend/proxy`;
+e}
/**
* Get the auth token from active UNode or global storage.