feat(app): add Android product flavors and env-based daemon config
This commit is contained in:
@@ -362,8 +362,21 @@ export default function SettingsScreen() {
|
||||
}, timeoutMs);
|
||||
|
||||
wsConnection.onopen = () => succeed();
|
||||
wsConnection.onerror = () => fail("Connection failed - check URL and network");
|
||||
wsConnection.onclose = () => fail("Connection failed - check URL and network");
|
||||
wsConnection.onerror = (event) => {
|
||||
const errorMessage =
|
||||
(event && typeof event === "object" && "message" in event && typeof (event as any).message === "string"
|
||||
? (event as any).message
|
||||
: null) || "Connection failed - check URL and network";
|
||||
console.error("[Settings] WebSocket test error", { url, errorMessage, event });
|
||||
fail(errorMessage);
|
||||
};
|
||||
wsConnection.onclose = (event) => {
|
||||
const reason =
|
||||
(event && typeof event.reason === "string" && event.reason.trim().length > 0 ? event.reason.trim() : null) ||
|
||||
`Connection failed (code ${event?.code ?? "unknown"})`;
|
||||
console.error("[Settings] WebSocket test closed", { url, code: event?.code, reason });
|
||||
fail(reason);
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -146,6 +146,57 @@ function createProfile(label: string, wsUrl: string): DaemonProfile {
|
||||
};
|
||||
}
|
||||
|
||||
type EnvDaemonConfig = {
|
||||
label?: string;
|
||||
wsUrl: string;
|
||||
restUrl?: string | null;
|
||||
};
|
||||
|
||||
function parseEnvDaemonDefaults(): DaemonProfile[] {
|
||||
const envDaemons = (() => {
|
||||
// Primary: allow a JSON array string like
|
||||
// EXPO_PUBLIC_DAEMONS='[{"label":"Host","wsUrl":"ws://10.0.0.1:6767/ws"}]'
|
||||
const jsonList = process.env.EXPO_PUBLIC_DAEMONS;
|
||||
if (jsonList) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonList) as EnvDaemonConfig[];
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[DaemonRegistry] Failed to parse EXPO_PUBLIC_DAEMONS:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: single host via EXPO_PUBLIC_DAEMON_WS_URL (+ optional label/rest URL)
|
||||
const singleWsUrl = process.env.EXPO_PUBLIC_DAEMON_WS_URL;
|
||||
if (singleWsUrl) {
|
||||
return [
|
||||
{
|
||||
label: process.env.EXPO_PUBLIC_DAEMON_LABEL,
|
||||
wsUrl: singleWsUrl,
|
||||
restUrl: process.env.EXPO_PUBLIC_DAEMON_REST_URL,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
})();
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
return envDaemons
|
||||
.filter((entry) => typeof entry?.wsUrl === "string" && entry.wsUrl.trim().length > 0)
|
||||
.map((entry) => ({
|
||||
id: generateDaemonId(),
|
||||
label: entry.label?.trim() || deriveLabelFromUrl(entry.wsUrl),
|
||||
wsUrl: entry.wsUrl.trim(),
|
||||
restUrl: entry.restUrl ?? null,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
metadata: null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadDaemonRegistryFromStorage(): Promise<DaemonProfile[]> {
|
||||
try {
|
||||
const stored = await AsyncStorage.getItem(REGISTRY_STORAGE_KEY);
|
||||
@@ -164,6 +215,12 @@ async function loadDaemonRegistryFromStorage(): Promise<DaemonProfile[]> {
|
||||
}
|
||||
}
|
||||
|
||||
const envDefaults = parseEnvDaemonDefaults();
|
||||
if (envDefaults.length > 0) {
|
||||
await AsyncStorage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(envDefaults));
|
||||
return envDefaults;
|
||||
}
|
||||
|
||||
const fallback = [createProfile("Local Host", FALLBACK_DAEMON_URL)];
|
||||
await AsyncStorage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(fallback));
|
||||
return fallback;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useState, useRef, ReactNode, useCallback, useEffect, useMemo } from "react";
|
||||
import { createContext, useState, useRef, ReactNode, useCallback, useEffect, useMemo } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useWebSocket, type UseWebSocketReturn } from "@/hooks/use-websocket";
|
||||
import { useDaemonRequest } from "@/hooks/use-daemon-request";
|
||||
@@ -378,20 +378,13 @@ export interface SessionContextValue {
|
||||
|
||||
const SessionContext = createContext<SessionContextValue | null>(null);
|
||||
|
||||
export function useSession() {
|
||||
const context = useContext(SessionContext);
|
||||
if (!context) {
|
||||
throw new Error("useSession must be used within SessionProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface SessionProviderProps {
|
||||
children: ReactNode;
|
||||
serverUrl: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
// SessionProvider feeds session state into Zustand; external consumers should use useDaemonSession or useSessionStore instead of accessing SessionContext directly.
|
||||
export function SessionProvider({ children, serverUrl, serverId }: SessionProviderProps) {
|
||||
const ws = useWebSocket(serverUrl);
|
||||
const wsIsConnected = ws.isConnected;
|
||||
|
||||
Reference in New Issue
Block a user