mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 06:12:51 +00:00
logs
This commit is contained in:
@@ -61,6 +61,7 @@
|
|||||||
"react-native": "0.81.4",
|
"react-native": "0.81.4",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-keyboard-controller": "1.18.5",
|
"react-native-keyboard-controller": "1.18.5",
|
||||||
|
"react-native-logs": "^5.5.0",
|
||||||
"react-native-mmkv": "^3.3.1",
|
"react-native-mmkv": "^3.3.1",
|
||||||
"react-native-reanimated": "~4.1.2",
|
"react-native-reanimated": "~4.1.2",
|
||||||
"react-native-safe-area-context": "~5.6.1",
|
"react-native-safe-area-context": "~5.6.1",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useAppForm, useFieldContext } from '@/components/form-components';
|
import { useAppForm, useFieldContext } from '@/components/form-components';
|
||||||
import { KeyList } from '@/components/key-manager/KeyList';
|
import { KeyList } from '@/components/key-manager/KeyList';
|
||||||
|
import { rootLogger } from '@/lib/logger';
|
||||||
import { useSshConnMutation } from '@/lib/query-fns';
|
import { useSshConnMutation } from '@/lib/query-fns';
|
||||||
import {
|
import {
|
||||||
connectionDetailsSchema,
|
connectionDetailsSchema,
|
||||||
@@ -23,6 +24,8 @@ import {
|
|||||||
import { useTheme } from '@/lib/theme';
|
import { useTheme } from '@/lib/theme';
|
||||||
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
|
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
|
||||||
|
|
||||||
|
const logger = rootLogger.extend('TabsIndex');
|
||||||
|
|
||||||
export default function TabsIndex() {
|
export default function TabsIndex() {
|
||||||
return <Host />;
|
return <Host />;
|
||||||
}
|
}
|
||||||
@@ -65,7 +68,7 @@ function Host() {
|
|||||||
const formErrors = useStore(connectionForm.store, (state) => state.errorMap);
|
const formErrors = useStore(connectionForm.store, (state) => state.errorMap);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formErrors || Object.keys(formErrors).length === 0) return;
|
if (!formErrors || Object.keys(formErrors).length === 0) return;
|
||||||
console.log('formErrors', JSON.stringify(formErrors, null, 2));
|
logger.info('formErrors', JSON.stringify(formErrors, null, 2));
|
||||||
}, [formErrors]);
|
}, [formErrors]);
|
||||||
|
|
||||||
const isSubmitting = useStore(
|
const isSubmitting = useStore(
|
||||||
@@ -207,7 +210,7 @@ function Host() {
|
|||||||
submittingTitle={buttonLabel}
|
submittingTitle={buttonLabel}
|
||||||
testID="connect"
|
testID="connect"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
console.log('Connect button pressed', { isSubmitting });
|
logger.info('Connect button pressed', { isSubmitting });
|
||||||
if (isSubmitting) return;
|
if (isSubmitting) return;
|
||||||
void connectionForm.handleSubmit();
|
void connectionForm.handleSubmit();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -14,11 +14,14 @@ import {
|
|||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
startTransition,
|
startTransition,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { KeyboardAvoidingView, Pressable, Text, View } from 'react-native';
|
import { KeyboardAvoidingView, Pressable, Text, View } from 'react-native';
|
||||||
|
import { rootLogger } from '@/lib/logger';
|
||||||
import { useSshStore } from '@/lib/ssh-store';
|
import { useSshStore } from '@/lib/ssh-store';
|
||||||
import { useTheme } from '@/lib/theme';
|
import { useTheme } from '@/lib/theme';
|
||||||
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
|
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
|
||||||
@@ -26,6 +29,8 @@ import { useContextSafe } from '@/lib/utils';
|
|||||||
|
|
||||||
type IconName = keyof typeof Ionicons.glyphMap;
|
type IconName = keyof typeof Ionicons.glyphMap;
|
||||||
|
|
||||||
|
const logger = rootLogger.extend('TabsShellDetail');
|
||||||
|
|
||||||
export default function TabsShellDetail() {
|
export default function TabsShellDetail() {
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
@@ -71,7 +76,6 @@ const encoder = new TextEncoder();
|
|||||||
|
|
||||||
function ShellDetail() {
|
function ShellDetail() {
|
||||||
const xtermRef = useRef<XtermWebViewHandle>(null);
|
const xtermRef = useRef<XtermWebViewHandle>(null);
|
||||||
const terminalReadyRef = useRef(false);
|
|
||||||
const listenerIdRef = useRef<bigint | null>(null);
|
const listenerIdRef = useRef<bigint | null>(null);
|
||||||
|
|
||||||
const searchParams = useLocalSearchParams<{
|
const searchParams = useLocalSearchParams<{
|
||||||
@@ -95,7 +99,7 @@ function ShellDetail() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shell && connection) return;
|
if (shell && connection) return;
|
||||||
console.log('shell or connection not found, replacing route with /shell');
|
logger.info('shell or connection not found, replacing route with /shell');
|
||||||
router.back();
|
router.back();
|
||||||
}, [connection, router, shell]);
|
}, [connection, router, shell]);
|
||||||
|
|
||||||
@@ -111,6 +115,37 @@ function ShellDetail() {
|
|||||||
|
|
||||||
const marginBottom = useBottomTabSpacing();
|
const marginBottom = useBottomTabSpacing();
|
||||||
|
|
||||||
|
const [modifierKeysActive, setModifierKeysActive] = useState<
|
||||||
|
KeyboardToolbarModifierButtonProps[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const sendBytes = useCallback(
|
||||||
|
(bytes: Uint8Array<ArrayBuffer>) => {
|
||||||
|
if (!shell) return;
|
||||||
|
|
||||||
|
modifierKeysActive
|
||||||
|
.sort((a, b) => a.orderPreference - b.orderPreference)
|
||||||
|
.forEach((m) => {
|
||||||
|
if (!m.canApplyModifierToBytes(bytes)) return;
|
||||||
|
bytes = m.applyModifierToBytes(bytes);
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.sendData(bytes.buffer).catch((e: unknown) => {
|
||||||
|
logger.warn('sendData failed', e);
|
||||||
|
router.back();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[shell, router, modifierKeysActive],
|
||||||
|
);
|
||||||
|
const toolbarContext: KeyboardToolbarContextType = useMemo(
|
||||||
|
() => ({
|
||||||
|
modifierKeysActive,
|
||||||
|
setModifierKeysActive,
|
||||||
|
sendBytes,
|
||||||
|
}),
|
||||||
|
[sendBytes, modifierKeysActive],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View
|
<View
|
||||||
@@ -133,11 +168,12 @@ function ShellDetail() {
|
|||||||
accessibilityLabel="Disconnect"
|
accessibilityLabel="Disconnect"
|
||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
|
logger.info('Disconnect button pressed');
|
||||||
if (!connection) return;
|
if (!connection) return;
|
||||||
try {
|
try {
|
||||||
await connection.disconnect();
|
await connection.disconnect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to disconnect', e);
|
logger.warn('Failed to disconnect', e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -151,93 +187,80 @@ function ShellDetail() {
|
|||||||
keyboardVerticalOffset={120}
|
keyboardVerticalOffset={120}
|
||||||
style={{ flex: 1, gap: 4 }}
|
style={{ flex: 1, gap: 4 }}
|
||||||
>
|
>
|
||||||
<View
|
<KeyboardToolBarContext value={toolbarContext}>
|
||||||
style={{
|
<View
|
||||||
flex: 1,
|
style={{
|
||||||
borderWidth: 2,
|
flex: 1,
|
||||||
borderColor: theme.colors.border,
|
borderWidth: 2,
|
||||||
}}
|
borderColor: theme.colors.border,
|
||||||
>
|
|
||||||
<XtermJsWebView
|
|
||||||
ref={xtermRef}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
webViewOptions={{
|
|
||||||
// Prevent iOS from adding automatic top inset inside WebView
|
|
||||||
contentInsetAdjustmentBehavior: 'never',
|
|
||||||
}}
|
}}
|
||||||
logger={{
|
>
|
||||||
log: console.log,
|
<XtermJsWebView
|
||||||
// debug: console.log,
|
ref={xtermRef}
|
||||||
warn: console.warn,
|
style={{ flex: 1 }}
|
||||||
error: console.error,
|
webViewOptions={{
|
||||||
}}
|
// Prevent iOS from adding automatic top inset inside WebView
|
||||||
// xterm options
|
contentInsetAdjustmentBehavior: 'never',
|
||||||
xtermOptions={{
|
}}
|
||||||
theme: {
|
logger={{
|
||||||
background: theme.colors.background,
|
log: logger.info,
|
||||||
foreground: theme.colors.textPrimary,
|
// debug: logger.debug,
|
||||||
},
|
warn: logger.warn,
|
||||||
}}
|
error: logger.error,
|
||||||
onInitialized={() => {
|
}}
|
||||||
if (terminalReadyRef.current) return;
|
xtermOptions={{
|
||||||
terminalReadyRef.current = true;
|
theme: {
|
||||||
|
background: theme.colors.background,
|
||||||
|
foreground: theme.colors.textPrimary,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onInitialized={() => {
|
||||||
|
if (!shell) throw new Error('Shell not found');
|
||||||
|
|
||||||
if (!shell) throw new Error('Shell not found');
|
// Replay from head, then attach live listener
|
||||||
|
void (async () => {
|
||||||
// Replay from head, then attach live listener
|
const res = shell.readBuffer({ mode: 'head' });
|
||||||
void (async () => {
|
logger.info('readBuffer(head)', {
|
||||||
const res = shell.readBuffer({ mode: 'head' });
|
chunks: res.chunks.length,
|
||||||
console.log('readBuffer(head)', {
|
nextSeq: res.nextSeq,
|
||||||
chunks: res.chunks.length,
|
dropped: res.dropped,
|
||||||
nextSeq: res.nextSeq,
|
});
|
||||||
dropped: res.dropped,
|
if (res.chunks.length) {
|
||||||
});
|
const chunks = res.chunks.map((c) => c.bytes);
|
||||||
if (res.chunks.length) {
|
const xr = xtermRef.current;
|
||||||
const chunks = res.chunks.map((c) => c.bytes);
|
if (xr) {
|
||||||
const xr = xtermRef.current;
|
xr.writeMany(chunks.map((c) => new Uint8Array(c)));
|
||||||
if (xr) {
|
xr.flush();
|
||||||
xr.writeMany(chunks.map((c) => new Uint8Array(c)));
|
|
||||||
xr.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const id = shell.addListener(
|
|
||||||
(ev: ListenerEvent) => {
|
|
||||||
if ('kind' in ev) {
|
|
||||||
console.log('listener.dropped', ev);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const chunk = ev;
|
}
|
||||||
const xr3 = xtermRef.current;
|
const id = shell.addListener(
|
||||||
if (xr3) xr3.write(new Uint8Array(chunk.bytes));
|
(ev: ListenerEvent) => {
|
||||||
},
|
if ('kind' in ev) {
|
||||||
{ cursor: { mode: 'seq', seq: res.nextSeq } },
|
logger.warn('listener.dropped', ev);
|
||||||
);
|
return;
|
||||||
console.log('shell listener attached', id.toString());
|
}
|
||||||
listenerIdRef.current = id;
|
const chunk = ev;
|
||||||
})();
|
const xr3 = xtermRef.current;
|
||||||
// Focus to pop the keyboard (iOS needs the prop we set)
|
if (xr3) xr3.write(new Uint8Array(chunk.bytes));
|
||||||
const xr2 = xtermRef.current;
|
},
|
||||||
if (xr2) xr2.focus();
|
{ cursor: { mode: 'seq', seq: res.nextSeq } },
|
||||||
}}
|
);
|
||||||
onData={(terminalMessage) => {
|
logger.info('shell listener attached', id.toString());
|
||||||
if (!shell) return;
|
listenerIdRef.current = id;
|
||||||
const bytes = encoder.encode(terminalMessage);
|
})();
|
||||||
shell.sendData(bytes.buffer).catch((e: unknown) => {
|
// Focus to pop the keyboard (iOS needs the prop we set)
|
||||||
console.warn('sendData failed', e);
|
const xr2 = xtermRef.current;
|
||||||
router.back();
|
if (xr2) xr2.focus();
|
||||||
});
|
}}
|
||||||
}}
|
onData={(terminalMessage) => {
|
||||||
/>
|
if (!shell) return;
|
||||||
</View>
|
const bytes = encoder.encode(terminalMessage);
|
||||||
<KeyboardToolbar
|
sendBytes(bytes);
|
||||||
sendBytes={(bytes) => {
|
}}
|
||||||
if (!shell) return;
|
/>
|
||||||
shell.sendData(bytes.buffer).catch((e: unknown) => {
|
</View>
|
||||||
console.warn('sendData failed', e);
|
<KeyboardToolbar />
|
||||||
router.back();
|
</KeyboardToolBarContext>
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</View>
|
</View>
|
||||||
{/* <KeyboardToolbar
|
{/* <KeyboardToolbar
|
||||||
@@ -249,39 +272,43 @@ function ShellDetail() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyboardToolbarProps = {
|
type KeyboardToolbarContextType = {
|
||||||
|
modifierKeysActive: KeyboardToolbarModifierButtonProps[];
|
||||||
|
setModifierKeysActive: React.Dispatch<
|
||||||
|
React.SetStateAction<KeyboardToolbarModifierButtonProps[]>
|
||||||
|
>;
|
||||||
sendBytes: (bytes: Uint8Array<ArrayBuffer>) => void;
|
sendBytes: (bytes: Uint8Array<ArrayBuffer>) => void;
|
||||||
};
|
};
|
||||||
const KeyboardToolBarContext = createContext<KeyboardToolbarProps | null>(null);
|
const KeyboardToolBarContext = createContext<KeyboardToolbarContextType | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
function KeyboardToolbar(props: KeyboardToolbarProps) {
|
function KeyboardToolbar() {
|
||||||
return (
|
return (
|
||||||
<KeyboardToolBarContext value={props}>
|
<View
|
||||||
<View
|
style={{
|
||||||
style={{
|
height: 100,
|
||||||
height: 100,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<KeyboardToolbarRow>
|
||||||
<KeyboardToolbarRow>
|
<KeyboardToolbarButtonPreset preset="esc" />
|
||||||
<KeyboardToolbarButtonPreset preset="esc" />
|
<KeyboardToolbarButtonPreset preset="/" />
|
||||||
<KeyboardToolbarButtonPreset preset="/" />
|
<KeyboardToolbarButtonPreset preset="|" />
|
||||||
<KeyboardToolbarButtonPreset preset="|" />
|
<KeyboardToolbarButtonPreset preset="home" />
|
||||||
<KeyboardToolbarButtonPreset preset="home" />
|
<KeyboardToolbarButtonPreset preset="up" />
|
||||||
<KeyboardToolbarButtonPreset preset="up" />
|
<KeyboardToolbarButtonPreset preset="end" />
|
||||||
<KeyboardToolbarButtonPreset preset="end" />
|
<KeyboardToolbarButtonPreset preset="pgup" />
|
||||||
<KeyboardToolbarButtonPreset preset="pgup" />
|
</KeyboardToolbarRow>
|
||||||
</KeyboardToolbarRow>
|
<KeyboardToolbarRow>
|
||||||
<KeyboardToolbarRow>
|
<KeyboardToolbarButtonPreset preset="tab" />
|
||||||
<KeyboardToolbarButtonPreset preset="tab" />
|
<KeyboardToolbarButtonPreset preset="ctrl" />
|
||||||
<KeyboardToolbarButtonPreset preset="ctrl" />
|
<KeyboardToolbarButtonPreset preset="alt" />
|
||||||
<KeyboardToolbarButtonPreset preset="alt" />
|
<KeyboardToolbarButtonPreset preset="left" />
|
||||||
<KeyboardToolbarButtonPreset preset="left" />
|
<KeyboardToolbarButtonPreset preset="down" />
|
||||||
<KeyboardToolbarButtonPreset preset="down" />
|
<KeyboardToolbarButtonPreset preset="right" />
|
||||||
<KeyboardToolbarButtonPreset preset="right" />
|
<KeyboardToolbarButtonPreset preset="pgdn" />
|
||||||
<KeyboardToolbarButtonPreset preset="pgdn" />
|
</KeyboardToolbarRow>
|
||||||
</KeyboardToolbarRow>
|
</View>
|
||||||
</View>
|
|
||||||
</KeyboardToolBarContext>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +348,61 @@ function KeyboardToolbarButtonPreset({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModifierContract = {
|
||||||
|
canApplyModifierToBytes: (bytes: Uint8Array<ArrayBuffer>) => boolean;
|
||||||
|
applyModifierToBytes: (
|
||||||
|
bytes: Uint8Array<ArrayBuffer>,
|
||||||
|
) => Uint8Array<ArrayBuffer>;
|
||||||
|
orderPreference: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const noOpModifier: ModifierContract = {
|
||||||
|
canApplyModifierToBytes: (_) => false,
|
||||||
|
applyModifierToBytes: (bytes) => bytes,
|
||||||
|
orderPreference: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeByte = 27;
|
||||||
|
|
||||||
|
const ctrlModifier: ModifierContract = {
|
||||||
|
orderPreference: 10,
|
||||||
|
canApplyModifierToBytes: (bytes) => {
|
||||||
|
const firstByte = bytes[0];
|
||||||
|
if (firstByte === undefined) return false;
|
||||||
|
return mapByteToCtrl(firstByte) != null;
|
||||||
|
},
|
||||||
|
applyModifierToBytes: (bytes) => {
|
||||||
|
const firstByte = bytes[0];
|
||||||
|
if (firstByte === undefined) return bytes;
|
||||||
|
const ctrlByte = mapByteToCtrl(firstByte);
|
||||||
|
if (ctrlByte == null) return bytes;
|
||||||
|
return new Uint8Array([ctrlByte]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const altModifier: ModifierContract = {
|
||||||
|
orderPreference: 20,
|
||||||
|
canApplyModifierToBytes: (bytes) => {
|
||||||
|
return bytes.length > 0 && bytes[0] !== escapeByte;
|
||||||
|
},
|
||||||
|
applyModifierToBytes: (bytes) => {
|
||||||
|
const result = new Uint8Array(bytes.length + 1);
|
||||||
|
result[0] = escapeByte;
|
||||||
|
result.set(bytes, 1);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapByteToCtrl(byte: number): number | null {
|
||||||
|
if (byte === 32) return 0; // Ctrl+Space
|
||||||
|
const uppercase = byte & 0b1101_1111; // Fold to uppercase / control range
|
||||||
|
if (uppercase >= 64 && uppercase <= 95) {
|
||||||
|
return uppercase & 0x1f;
|
||||||
|
}
|
||||||
|
if (byte === 63) return 127; // Ctrl+?
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const keyboardToolbarButtonPresetToProps: Record<
|
const keyboardToolbarButtonPresetToProps: Record<
|
||||||
KeyboardToolbarButtonPresetType,
|
KeyboardToolbarButtonPresetType,
|
||||||
KeyboardToolbarButtonProps
|
KeyboardToolbarButtonProps
|
||||||
@@ -332,10 +414,7 @@ const keyboardToolbarButtonPresetToProps: Record<
|
|||||||
end: { label: 'END', sendBytes: new Uint8Array([27, 91, 70]) },
|
end: { label: 'END', sendBytes: new Uint8Array([27, 91, 70]) },
|
||||||
pgup: { label: 'PGUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
|
pgup: { label: 'PGUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
|
||||||
pgdn: { label: 'PGDN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
|
pgdn: { label: 'PGDN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
|
||||||
fn: { label: 'FN', isModifier: true },
|
|
||||||
tab: { label: 'TAB', sendBytes: new Uint8Array([9]) },
|
tab: { label: 'TAB', sendBytes: new Uint8Array([9]) },
|
||||||
ctrl: { label: 'CTRL', isModifier: true },
|
|
||||||
alt: { label: 'ALT', isModifier: true },
|
|
||||||
left: { iconName: 'arrow-back', sendBytes: new Uint8Array([27, 91, 68]) },
|
left: { iconName: 'arrow-back', sendBytes: new Uint8Array([27, 91, 68]) },
|
||||||
up: { iconName: 'arrow-up', sendBytes: new Uint8Array([27, 91, 65]) },
|
up: { iconName: 'arrow-up', sendBytes: new Uint8Array([27, 91, 65]) },
|
||||||
down: { iconName: 'arrow-down', sendBytes: new Uint8Array([27, 91, 66]) },
|
down: { iconName: 'arrow-down', sendBytes: new Uint8Array([27, 91, 66]) },
|
||||||
@@ -347,29 +426,40 @@ const keyboardToolbarButtonPresetToProps: Record<
|
|||||||
delete: { label: 'DELETE', sendBytes: new Uint8Array([27, 91, 51, 126]) },
|
delete: { label: 'DELETE', sendBytes: new Uint8Array([27, 91, 51, 126]) },
|
||||||
pageup: { label: 'PAGEUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
|
pageup: { label: 'PAGEUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
|
||||||
pagedown: { label: 'PAGEDOWN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
|
pagedown: { label: 'PAGEDOWN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
|
||||||
|
fn: {
|
||||||
|
label: 'FN',
|
||||||
|
type: 'modifier',
|
||||||
|
...noOpModifier,
|
||||||
|
},
|
||||||
|
ctrl: { label: 'CTRL', type: 'modifier', ...ctrlModifier },
|
||||||
|
alt: { label: 'ALT', type: 'modifier', ...altModifier },
|
||||||
};
|
};
|
||||||
|
|
||||||
type KeyboardToolbarButtonProps = (
|
type KeyboardToolbarButtonViewProps =
|
||||||
| {
|
| {
|
||||||
isModifier: true;
|
label: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
sendBytes: Uint8Array;
|
iconName: IconName;
|
||||||
}
|
};
|
||||||
) &
|
|
||||||
(
|
type KeyboardToolbarModifierButtonProps = {
|
||||||
| {
|
type: 'modifier';
|
||||||
label: string;
|
} & ModifierContract &
|
||||||
}
|
KeyboardToolbarButtonViewProps;
|
||||||
| {
|
type KeyboardToolbarInstantButtonProps = {
|
||||||
iconName: IconName;
|
type?: 'sendBytes';
|
||||||
}
|
sendBytes: Uint8Array<ArrayBuffer>;
|
||||||
);
|
} & KeyboardToolbarButtonViewProps;
|
||||||
|
|
||||||
|
type KeyboardToolbarButtonProps =
|
||||||
|
| KeyboardToolbarModifierButtonProps
|
||||||
|
| KeyboardToolbarInstantButtonProps;
|
||||||
|
|
||||||
function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
|
function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [modifierActive, setModifierActive] = useState(false);
|
const { sendBytes, modifierKeysActive, setModifierKeysActive } =
|
||||||
const { sendBytes } = useContextSafe(KeyboardToolBarContext);
|
useContextSafe(KeyboardToolBarContext);
|
||||||
|
|
||||||
const isTextLabel = 'label' in props;
|
const isTextLabel = 'label' in props;
|
||||||
const children = isTextLabel ? (
|
const children = isTextLabel ? (
|
||||||
@@ -382,16 +472,26 @@ function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const modifierActive =
|
||||||
|
props.type === 'modifier' && modifierKeysActive.includes(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
console.log('button pressed');
|
if (props.type === 'modifier') {
|
||||||
if ('isModifier' in props && props.isModifier) {
|
setModifierKeysActive((modifierKeysActive) =>
|
||||||
setModifierActive((active) => !active);
|
modifierKeysActive.includes(props)
|
||||||
} else if ('sendBytes' in props) {
|
? modifierKeysActive.filter((m) => m !== props)
|
||||||
// todo: send key press
|
: [...modifierKeysActive, props],
|
||||||
sendBytes(new Uint8Array(props.sendBytes));
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('sendBytes' in props) {
|
||||||
|
sendBytes(new Uint8Array(props.sendBytes));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error('Invalid button type');
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,11 +17,14 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { rootLogger } from '@/lib/logger';
|
||||||
import { preferences } from '@/lib/preferences';
|
import { preferences } from '@/lib/preferences';
|
||||||
import {} from '@/lib/query-fns';
|
import {} from '@/lib/query-fns';
|
||||||
import { useSshStore } from '@/lib/ssh-store';
|
import { useSshStore } from '@/lib/ssh-store';
|
||||||
import { useTheme } from '@/lib/theme';
|
import { useTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
const logger = rootLogger.extend('TabsShellList');
|
||||||
|
|
||||||
export default function TabsShellList() {
|
export default function TabsShellList() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
@@ -35,7 +38,7 @@ function ShellContent() {
|
|||||||
const connections = useSshStore(
|
const connections = useSshStore(
|
||||||
useShallow((s) => Object.values(s.connections)),
|
useShallow((s) => Object.values(s.connections)),
|
||||||
);
|
);
|
||||||
console.log('DEBUG list view connections', connections.length);
|
logger.debug('list view connections', connections.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { isLiquidGlassAvailable } from 'expo-glass-effect';
|
|||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
||||||
|
import { rootLogger } from '@/lib/logger';
|
||||||
import { ThemeProvider } from '../lib/theme';
|
import { ThemeProvider } from '../lib/theme';
|
||||||
import { queryClient } from '../lib/utils';
|
import { queryClient } from '../lib/utils';
|
||||||
|
|
||||||
console.log('Fressh App Init', {
|
rootLogger.info('Fressh App Init', {
|
||||||
isLiquidGlassAvailable: isLiquidGlassAvailable(),
|
isLiquidGlassAvailable: isLiquidGlassAvailable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
void DevClient.registerDevMenuItems([
|
void DevClient.registerDevMenuItems([
|
||||||
{
|
{
|
||||||
callback: () => {
|
callback: () => {
|
||||||
console.log('Hello from dev menu');
|
rootLogger.info('Hello from dev menu');
|
||||||
},
|
},
|
||||||
name: 'Hello from dev menu',
|
name: 'Hello from dev menu',
|
||||||
},
|
},
|
||||||
|
|||||||
25
apps/mobile/src/lib/logger.ts
Normal file
25
apps/mobile/src/lib/logger.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { logger, consoleTransport } from "react-native-logs";
|
||||||
|
|
||||||
|
export const rootLogger = logger.createLogger({
|
||||||
|
levels: {
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3,
|
||||||
|
},
|
||||||
|
severity: "debug",
|
||||||
|
transport: consoleTransport,
|
||||||
|
transportOptions: {
|
||||||
|
colors: {
|
||||||
|
info: "blueBright",
|
||||||
|
warn: "yellowBright",
|
||||||
|
error: "redBright",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async: true,
|
||||||
|
dateFormat: "time",
|
||||||
|
printLevel: true,
|
||||||
|
printDate: true,
|
||||||
|
fixedExtLvlLength: false,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { type SshConnectionProgress } from '@fressh/react-native-uniffi-russh';
|
import { type SshConnectionProgress } from '@fressh/react-native-uniffi-russh';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
import { rootLogger } from './logger';
|
||||||
import { secretsManager, type InputConnectionDetails } from './secrets-manager';
|
import { secretsManager, type InputConnectionDetails } from './secrets-manager';
|
||||||
import { useSshStore } from './ssh-store';
|
import { useSshStore } from './ssh-store';
|
||||||
import { AbortSignalTimeout } from './utils';
|
import { AbortSignalTimeout } from './utils';
|
||||||
|
|
||||||
|
const logger = rootLogger.extend('QueryFns');
|
||||||
|
|
||||||
export const useSshConnMutation = (opts?: {
|
export const useSshConnMutation = (opts?: {
|
||||||
onConnectionProgress?: (progressEvent: SshConnectionProgress) => void;
|
onConnectionProgress?: (progressEvent: SshConnectionProgress) => void;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -14,20 +17,20 @@ export const useSshConnMutation = (opts?: {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (connectionDetails: InputConnectionDetails) => {
|
mutationFn: async (connectionDetails: InputConnectionDetails) => {
|
||||||
try {
|
try {
|
||||||
console.log('Connecting to SSH server...');
|
logger.info('Connecting to SSH server...');
|
||||||
// Resolve security into the RN bridge shape
|
// Resolve security into the RN bridge shape
|
||||||
const security =
|
const security =
|
||||||
connectionDetails.security.type === 'password'
|
connectionDetails.security.type === 'password'
|
||||||
? {
|
? {
|
||||||
type: 'password' as const,
|
type: 'password' as const,
|
||||||
password: connectionDetails.security.password,
|
password: connectionDetails.security.password,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'key' as const,
|
type: 'key' as const,
|
||||||
privateKey: await secretsManager.keys.utils
|
privateKey: await secretsManager.keys.utils
|
||||||
.getPrivateKey(connectionDetails.security.keyId)
|
.getPrivateKey(connectionDetails.security.keyId)
|
||||||
.then((e) => e.value),
|
.then((e) => e.value),
|
||||||
};
|
};
|
||||||
|
|
||||||
const sshConnection = await connect({
|
const sshConnection = await connect({
|
||||||
host: connectionDetails.host,
|
host: connectionDetails.host,
|
||||||
@@ -35,11 +38,11 @@ export const useSshConnMutation = (opts?: {
|
|||||||
username: connectionDetails.username,
|
username: connectionDetails.username,
|
||||||
security,
|
security,
|
||||||
onConnectionProgress: (progressEvent) => {
|
onConnectionProgress: (progressEvent) => {
|
||||||
console.log('SSH connect progress event', progressEvent);
|
logger.info('SSH connect progress event', progressEvent);
|
||||||
opts?.onConnectionProgress?.(progressEvent);
|
opts?.onConnectionProgress?.(progressEvent);
|
||||||
},
|
},
|
||||||
onServerKey: async (serverKeyInfo) => {
|
onServerKey: async (serverKeyInfo) => {
|
||||||
console.log('SSH server key', serverKeyInfo);
|
logger.info('SSH server key', serverKeyInfo);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
abortSignal: AbortSignalTimeout(5_000),
|
abortSignal: AbortSignalTimeout(5_000),
|
||||||
@@ -55,7 +58,7 @@ export const useSshConnMutation = (opts?: {
|
|||||||
abortSignal: AbortSignalTimeout(5_000),
|
abortSignal: AbortSignalTimeout(5_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'Connected to SSH server',
|
'Connected to SSH server',
|
||||||
sshConnection.connectionId,
|
sshConnection.connectionId,
|
||||||
shellHandle.channelId,
|
shellHandle.channelId,
|
||||||
@@ -68,7 +71,7 @@ export const useSshConnMutation = (opts?: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting to SSH server', error);
|
logger.error('Error connecting to SSH server', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,14 +3,10 @@ import { queryOptions } from '@tanstack/react-query';
|
|||||||
import * as Crypto from 'expo-crypto';
|
import * as Crypto from 'expo-crypto';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
import { rootLogger } from './logger';
|
||||||
import { queryClient, type StrictOmit } from './utils';
|
import { queryClient, type StrictOmit } from './utils';
|
||||||
|
|
||||||
const shouldLog = false as boolean;
|
const logger = rootLogger.extend('SecretsManager');
|
||||||
const log = (...args: Parameters<typeof console.log>) => {
|
|
||||||
if (shouldLog) {
|
|
||||||
console.log(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function splitIntoChunks(data: string, chunkSize: number): string[] {
|
function splitIntoChunks(data: string, chunkSize: number): string[] {
|
||||||
const chunks: string[] = [];
|
const chunks: string[] = [];
|
||||||
@@ -74,17 +70,17 @@ function makeBetterSecureStore<
|
|||||||
const rawRootManifestString =
|
const rawRootManifestString =
|
||||||
await SecureStore.getItemAsync(rootManifestKey);
|
await SecureStore.getItemAsync(rootManifestKey);
|
||||||
|
|
||||||
log('DEBUG rawRootManifestString', rawRootManifestString);
|
logger.debug('rawRootManifestString', rawRootManifestString);
|
||||||
|
|
||||||
log(
|
logger.info(
|
||||||
`Root manifest for ${rootManifestKey} is ${rawRootManifestString?.length ?? 0} bytes`,
|
`Root manifest for ${rootManifestKey} is ${rawRootManifestString?.length ?? 0} bytes`,
|
||||||
);
|
);
|
||||||
const unsafedRootManifest: unknown = rawRootManifestString
|
const unsafedRootManifest: unknown = rawRootManifestString
|
||||||
? JSON.parse(rawRootManifestString)
|
? JSON.parse(rawRootManifestString)
|
||||||
: {
|
: {
|
||||||
manifestVersion: rootManifestVersion,
|
manifestVersion: rootManifestVersion,
|
||||||
manifestChunksIds: [],
|
manifestChunksIds: [],
|
||||||
};
|
};
|
||||||
const rootManifest = rootManifestSchema.parse(unsafedRootManifest);
|
const rootManifest = rootManifestSchema.parse(unsafedRootManifest);
|
||||||
const manifestChunks = await Promise.all(
|
const manifestChunks = await Promise.all(
|
||||||
rootManifest.manifestChunksIds.map(async (manifestChunkId) => {
|
rootManifest.manifestChunksIds.map(async (manifestChunkId) => {
|
||||||
@@ -94,7 +90,7 @@ function makeBetterSecureStore<
|
|||||||
);
|
);
|
||||||
if (!rawManifestChunkString)
|
if (!rawManifestChunkString)
|
||||||
throw new Error('Manifest chunk not found');
|
throw new Error('Manifest chunk not found');
|
||||||
log(
|
logger.info(
|
||||||
`Manifest chunk for ${manifestChunkKeyString} is ${rawManifestChunkString.length} bytes`,
|
`Manifest chunk for ${manifestChunkKeyString} is ${rawManifestChunkString.length} bytes`,
|
||||||
);
|
);
|
||||||
const unsafedManifestChunk: unknown = JSON.parse(
|
const unsafedManifestChunk: unknown = JSON.parse(
|
||||||
@@ -120,7 +116,7 @@ function makeBetterSecureStore<
|
|||||||
Array.from({ length: manifestEntry.chunkCount }, async (_, chunkIdx) => {
|
Array.from({ length: manifestEntry.chunkCount }, async (_, chunkIdx) => {
|
||||||
const entryKeyString = entryKey(manifestEntry.id, chunkIdx);
|
const entryKeyString = entryKey(manifestEntry.id, chunkIdx);
|
||||||
const rawEntryChunk = await SecureStore.getItemAsync(entryKeyString);
|
const rawEntryChunk = await SecureStore.getItemAsync(entryKeyString);
|
||||||
log(
|
logger.info(
|
||||||
`Entry chunk for ${entryKeyString} is ${rawEntryChunk?.length} bytes`,
|
`Entry chunk for ${entryKeyString} is ${rawEntryChunk?.length} bytes`,
|
||||||
);
|
);
|
||||||
if (!rawEntryChunk) throw new Error('Entry chunk not found');
|
if (!rawEntryChunk) throw new Error('Entry chunk not found');
|
||||||
@@ -206,7 +202,7 @@ function makeBetterSecureStore<
|
|||||||
(mChunk) => mChunk.manifestChunk.entries.length === 0,
|
(mChunk) => mChunk.manifestChunk.entries.length === 0,
|
||||||
);
|
);
|
||||||
if (emptyManifestChunks.length > 0) {
|
if (emptyManifestChunks.length > 0) {
|
||||||
log('DEBUG: removing empty manifest chunks', emptyManifestChunks.length);
|
logger.debug('removing empty manifest chunks', emptyManifestChunks.length);
|
||||||
manifest.rootManifest.manifestChunksIds =
|
manifest.rootManifest.manifestChunksIds =
|
||||||
manifest.rootManifest.manifestChunksIds.filter(
|
manifest.rootManifest.manifestChunksIds.filter(
|
||||||
(mChunkId) =>
|
(mChunkId) =>
|
||||||
@@ -234,7 +230,7 @@ function makeBetterSecureStore<
|
|||||||
value: string;
|
value: string;
|
||||||
}) {
|
}) {
|
||||||
await deleteEntry(params.id).catch(() => {
|
await deleteEntry(params.id).catch(() => {
|
||||||
log(`Entry ${params.id} not found, creating new one`);
|
logger.info(`Entry ${params.id} not found, creating new one`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const valueChunks = splitIntoChunks(params.value, sizeLimit);
|
const valueChunks = splitIntoChunks(params.value, sizeLimit);
|
||||||
@@ -251,7 +247,7 @@ function makeBetterSecureStore<
|
|||||||
const existingManifestChunkWithRoom = manifest.manifestChunks.find(
|
const existingManifestChunkWithRoom = manifest.manifestChunks.find(
|
||||||
(mChunk) => sizeLimit > mChunk.manifestChunkSize + newManifestEntrySize,
|
(mChunk) => sizeLimit > mChunk.manifestChunkSize + newManifestEntrySize,
|
||||||
);
|
);
|
||||||
log('DEBUG existingManifestChunkWithRoom', existingManifestChunkWithRoom);
|
logger.debug('existingManifestChunkWithRoom', existingManifestChunkWithRoom);
|
||||||
const manifestChunkWithRoom =
|
const manifestChunkWithRoom =
|
||||||
existingManifestChunkWithRoom ??
|
existingManifestChunkWithRoom ??
|
||||||
(await (async () => {
|
(await (async () => {
|
||||||
@@ -263,7 +259,7 @@ function makeBetterSecureStore<
|
|||||||
manifestChunkId: Crypto.randomUUID(),
|
manifestChunkId: Crypto.randomUUID(),
|
||||||
manifestChunkSize: 0,
|
manifestChunkSize: 0,
|
||||||
} satisfies NonNullable<(typeof manifest.manifestChunks)[number]>;
|
} satisfies NonNullable<(typeof manifest.manifestChunks)[number]>;
|
||||||
log(`Adding new manifest chunk ${newManifestChunk.manifestChunkId}`);
|
logger.info(`Adding new manifest chunk ${newManifestChunk.manifestChunkId}`);
|
||||||
manifest.rootManifest.manifestChunksIds.push(
|
manifest.rootManifest.manifestChunksIds.push(
|
||||||
newManifestChunk.manifestChunkId,
|
newManifestChunk.manifestChunkId,
|
||||||
);
|
);
|
||||||
@@ -271,7 +267,7 @@ function makeBetterSecureStore<
|
|||||||
rootManifestKey,
|
rootManifestKey,
|
||||||
JSON.stringify(manifest.rootManifest),
|
JSON.stringify(manifest.rootManifest),
|
||||||
);
|
);
|
||||||
log('DEBUG: newRootManifest', manifest.rootManifest);
|
logger.debug('newRootManifest', manifest.rootManifest);
|
||||||
return newManifestChunk;
|
return newManifestChunk;
|
||||||
})());
|
})());
|
||||||
|
|
||||||
@@ -284,15 +280,15 @@ function makeBetterSecureStore<
|
|||||||
manifestChunkKeyString,
|
manifestChunkKeyString,
|
||||||
JSON.stringify(manifestChunkWithRoom.manifestChunk),
|
JSON.stringify(manifestChunkWithRoom.manifestChunk),
|
||||||
).then(() => {
|
).then(() => {
|
||||||
log(
|
logger.info(
|
||||||
`Set manifest chunk for ${manifestChunkKeyString} to ${JSON.stringify(manifestChunkWithRoom.manifestChunk).length} bytes`,
|
`Set manifest chunk for ${manifestChunkKeyString} to ${JSON.stringify(manifestChunkWithRoom.manifestChunk).length} bytes`,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
...valueChunks.map(async (vChunk, chunkIdx) => {
|
...valueChunks.map(async (vChunk, chunkIdx) => {
|
||||||
const entryKeyString = entryKey(newManifestEntry.id, chunkIdx);
|
const entryKeyString = entryKey(newManifestEntry.id, chunkIdx);
|
||||||
console.log('DEBUG: setting entry chunk', entryKeyString);
|
logger.debug('setting entry chunk', entryKeyString);
|
||||||
await SecureStore.setItemAsync(entryKeyString, vChunk);
|
await SecureStore.setItemAsync(entryKeyString, vChunk);
|
||||||
log(
|
logger.info(
|
||||||
`Set entry chunk for ${entryKeyString} ${chunkIdx} to ${vChunk.length} bytes`,
|
`Set entry chunk for ${entryKeyString} ${chunkIdx} to ${vChunk.length} bytes`,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -332,15 +328,15 @@ async function upsertPrivateKey(params: {
|
|||||||
}) {
|
}) {
|
||||||
const validateKeyResult = RnRussh.validatePrivateKey(params.value);
|
const validateKeyResult = RnRussh.validatePrivateKey(params.value);
|
||||||
if (!validateKeyResult.valid) {
|
if (!validateKeyResult.valid) {
|
||||||
console.log('Invalid private key', validateKeyResult.error);
|
logger.info('Invalid private key', validateKeyResult.error);
|
||||||
if (validateKeyResult.error.tag === SshError_Tags.RusshKeys) {
|
if (validateKeyResult.error.tag === SshError_Tags.RusshKeys) {
|
||||||
console.log('Invalid private key inner', validateKeyResult.error.inner);
|
logger.info('Invalid private key inner', validateKeyResult.error.inner);
|
||||||
console.log('Invalid private key content', params.value);
|
logger.info('Invalid private key content', params.value);
|
||||||
}
|
}
|
||||||
throw new Error('Invalid private key', { cause: validateKeyResult.error });
|
throw new Error('Invalid private key', { cause: validateKeyResult.error });
|
||||||
}
|
}
|
||||||
const keyId = params.keyId ?? `key_${Crypto.randomUUID()}`;
|
const keyId = params.keyId ?? `key_${Crypto.randomUUID()}`;
|
||||||
log(`${params.keyId ? 'Upserting' : 'Creating'} private key ${keyId}`);
|
logger.info(`${params.keyId ? 'Upserting' : 'Creating'} private key ${keyId}`);
|
||||||
// Preserve createdAtMs if the entry already exists
|
// Preserve createdAtMs if the entry already exists
|
||||||
const existing = await betterKeyStorage
|
const existing = await betterKeyStorage
|
||||||
.getEntry(keyId)
|
.getEntry(keyId)
|
||||||
@@ -356,7 +352,7 @@ async function upsertPrivateKey(params: {
|
|||||||
},
|
},
|
||||||
value: params.value,
|
value: params.value,
|
||||||
});
|
});
|
||||||
log('DEBUG: invalidating key query');
|
logger.debug('invalidating key query');
|
||||||
await queryClient.invalidateQueries({ queryKey: [keyQueryKey] });
|
await queryClient.invalidateQueries({ queryKey: [keyQueryKey] });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +367,7 @@ const listKeysQueryOptions = queryOptions({
|
|||||||
queryKey: [keyQueryKey],
|
queryKey: [keyQueryKey],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const results = await betterKeyStorage.listEntriesWithValues();
|
const results = await betterKeyStorage.listEntriesWithValues();
|
||||||
log(`Listed ${results.length} private keys`);
|
logger.info(`Listed ${results.length} private keys`);
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -431,7 +427,7 @@ async function upsertConnection(params: {
|
|||||||
},
|
},
|
||||||
value: JSON.stringify(params.details),
|
value: JSON.stringify(params.details),
|
||||||
});
|
});
|
||||||
log('DEBUG: invalidating connection query');
|
logger.debug('invalidating connection query');
|
||||||
await queryClient.invalidateQueries({ queryKey: [connectionQueryKey] });
|
await queryClient.invalidateQueries({ queryKey: [connectionQueryKey] });
|
||||||
return params.details;
|
return params.details;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import {
|
|||||||
type SshShell,
|
type SshShell,
|
||||||
} from '@fressh/react-native-uniffi-russh';
|
} from '@fressh/react-native-uniffi-russh';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { rootLogger } from './logger';
|
||||||
|
|
||||||
|
const logger = rootLogger.extend('SshStore');
|
||||||
|
|
||||||
type SshRegistryStore = {
|
type SshRegistryStore = {
|
||||||
connections: Record<string, SshConnection>;
|
connections: Record<string, SshConnection>;
|
||||||
@@ -19,7 +22,7 @@ export const useSshStore = create<SshRegistryStore>((set) => ({
|
|||||||
...args,
|
...args,
|
||||||
onDisconnected: (connectionId) => {
|
onDisconnected: (connectionId) => {
|
||||||
args.onDisconnected?.(connectionId);
|
args.onDisconnected?.(connectionId);
|
||||||
console.log('DEBUG connection disconnected', connectionId);
|
logger.debug('connection disconnected', connectionId);
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const { [connectionId]: _omit, ...rest } = s.connections;
|
const { [connectionId]: _omit, ...rest } = s.connections;
|
||||||
return { connections: rest };
|
return { connections: rest };
|
||||||
@@ -33,7 +36,7 @@ export const useSshStore = create<SshRegistryStore>((set) => ({
|
|||||||
onClosed: (channelId) => {
|
onClosed: (channelId) => {
|
||||||
args.onClosed?.(channelId);
|
args.onClosed?.(channelId);
|
||||||
const storeKey = `${connection.connectionId}-${channelId}` as const;
|
const storeKey = `${connection.connectionId}-${channelId}` as const;
|
||||||
console.log('DEBUG shell closed', storeKey);
|
logger.debug('shell closed', storeKey);
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const { [storeKey]: _omit, ...rest } = s.shells;
|
const { [storeKey]: _omit, ...rest } = s.shells;
|
||||||
if (Object.keys(rest).length === 0) {
|
if (Object.keys(rest).length === 0) {
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -151,6 +151,9 @@ importers:
|
|||||||
react-native-keyboard-controller:
|
react-native-keyboard-controller:
|
||||||
specifier: 1.18.5
|
specifier: 1.18.5
|
||||||
version: 1.18.5(react-native-reanimated@4.1.2(@babel/core@7.28.3)(react-native-worklets@0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
version: 1.18.5(react-native-reanimated@4.1.2(@babel/core@7.28.3)(react-native-worklets@0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||||
|
react-native-logs:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.5.0
|
||||||
react-native-mmkv:
|
react-native-mmkv:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
version: 3.3.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||||
@@ -7697,6 +7700,9 @@ packages:
|
|||||||
react-native: '*'
|
react-native: '*'
|
||||||
react-native-reanimated: '>=3.0.0'
|
react-native-reanimated: '>=3.0.0'
|
||||||
|
|
||||||
|
react-native-logs@5.5.0:
|
||||||
|
resolution: {integrity: sha512-H3Jc1pNTzNhYb9yHuk1drHdyGHwRvt4IERSz3EUul8vVTey6999fzGRFLK6ugrxYnmw7P+5fo/mRzDXeByhA8g==}
|
||||||
|
|
||||||
react-native-mmkv@3.3.1:
|
react-native-mmkv@3.3.1:
|
||||||
resolution: {integrity: sha512-LYamDWQirPTUJZ9Re+BkCD+zLRGNr+EVJDeIeblvoJXGatWy9PXnChtajDSLqwjX3EXVeUyjgrembs7wlBw9ug==}
|
resolution: {integrity: sha512-LYamDWQirPTUJZ9Re+BkCD+zLRGNr+EVJDeIeblvoJXGatWy9PXnChtajDSLqwjX3EXVeUyjgrembs7wlBw9ug==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -18495,6 +18501,8 @@ snapshots:
|
|||||||
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||||
react-native-reanimated: 4.1.2(@babel/core@7.28.3)(react-native-worklets@0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
react-native-reanimated: 4.1.2(@babel/core@7.28.3)(react-native-worklets@0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||||
|
|
||||||
|
react-native-logs@5.5.0: {}
|
||||||
|
|
||||||
react-native-mmkv@3.3.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0):
|
react-native-mmkv@3.3.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user