Add on-device logger

This commit is contained in:
2026-05-31 14:44:48 +02:00
parent b4c5578e8c
commit 2ebb4a6f01
4 changed files with 152 additions and 16 deletions
+18 -4
View File
@@ -4,7 +4,15 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
import CameraScreen from './src/screens/CameraScreen'; import CameraScreen from './src/screens/CameraScreen';
import PreviewScreen from './src/screens/PreviewScreen'; import PreviewScreen from './src/screens/PreviewScreen';
import AdminScreen from './src/screens/AdminScreen'; import AdminScreen from './src/screens/AdminScreen';
import { loadSettings, AppSettings } from './src/services/settings'; import { loadSettings, saveSettings, AppSettings } from './src/services/settings';
import { logger } from './src/services/logger';
// Set up global error handler
const defaultErrorHandler = (global as any).ErrorUtils.getGlobalHandler();
(global as any).ErrorUtils.setGlobalHandler(async (error: any, isFatal: boolean) => {
await logger.error(`Unhandled Exception (Fatal: ${isFatal})`, error);
defaultErrorHandler(error, isFatal);
});
import KioskMode from './modules/kiosk-mode'; import KioskMode from './modules/kiosk-mode';
type ScreenState = 'home' | 'camera' | 'preview' | 'admin'; type ScreenState = 'home' | 'camera' | 'preview' | 'admin';
@@ -42,9 +50,15 @@ export default function App() {
setCurrentScreen('camera'); setCurrentScreen('camera');
}; };
const handlePhotoCaptured = (uri: string) => { const handlePhotoCaptured = async (uri: string) => {
setPhotoUris((prev) => [...prev, uri]); await logger.log(`Photo captured via CameraScreen: ${uri}`);
setCurrentScreen('preview'); try {
setPhotoUris((prev) => [...prev, uri]);
setCurrentScreen('preview');
await logger.log('State updated to preview screen successfully.');
} catch (e) {
await logger.error('Error transitioning to preview screen:', e);
}
}; };
// Called by CameraScreen when burst mode completes all photos // Called by CameraScreen when burst mode completes all photos
+44 -12
View File
@@ -8,6 +8,7 @@ import {
ScrollView, ScrollView,
Switch, Switch,
Alert, Alert,
Modal,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
@@ -22,9 +23,10 @@ import {
faCalendarDay, faCalendarDay,
faImages, faImages,
faFloppyDisk, faFloppyDisk,
faChampagneGlasses, faFileLines,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { THEME } from '../styles/theme'; import { THEME } from '../styles/theme';
import { logger } from '../services/logger';
import KioskMode from '../../modules/kiosk-mode'; import KioskMode from '../../modules/kiosk-mode';
import { AppSettings, saveSettings } from '../services/settings'; import { AppSettings, saveSettings } from '../services/settings';
import { FRAMES } from '../data/frames'; import { FRAMES } from '../data/frames';
@@ -63,11 +65,11 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration)); const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration));
const [printerIp, setPrinterIp] = useState<string>(currentSettings.printerIp); const [printerIp, setPrinterIp] = useState<string>(currentSettings.printerIp);
const [password, setPassword] = useState<string>(currentSettings.adminPassword); const [password, setPassword] = useState<string>(currentSettings.adminPassword);
const [logs, setLogs] = useState<string | null>(null);
const [kioskActive, setKioskActive] = useState<boolean>(false); const [kioskActive, setKioskActive] = useState<boolean>(false);
const [isDeviceOwner, setIsDeviceOwner] = useState<boolean>(false); const [isDeviceOwner, setIsDeviceOwner] = useState<boolean>(false);
// New settings state
const [frameMode, setFrameMode] = useState<FrameMode>(currentSettings.frameMode); const [frameMode, setFrameMode] = useState<FrameMode>(currentSettings.frameMode);
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId); const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId);
const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []); const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
@@ -78,7 +80,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec)); const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText); const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
// Load Kiosk and Device Owner status on mount
useEffect(() => { useEffect(() => {
const checkKioskStatus = () => { const checkKioskStatus = () => {
try { try {
@@ -92,7 +93,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
}; };
checkKioskStatus(); checkKioskStatus();
// Poll status every second
const interval = setInterval(checkKioskStatus, 1000); const interval = setInterval(checkKioskStatus, 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
@@ -196,7 +196,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
} }
}; };
// Renders a 3-way toggle segment control
const renderSegment = <T extends string>( const renderSegment = <T extends string>(
options: { value: T; label: string }[], options: { value: T; label: string }[],
current: T, current: T,
@@ -228,10 +227,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} /> <FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text> <Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity
style={styles.logBtn}
onPress={async () => setLogs(await logger.readLogs())}
>
<FontAwesomeIcon icon={faFileLines} size={16} color={THEME.colors.text} />
<Text style={styles.closeBtnText}>Logs</Text>
</TouchableOpacity>
</View> </View>
<ScrollView contentContainerStyle={styles.scrollContent}> <ScrollView contentContainerStyle={styles.scrollContent}>
{/* ─── Printer Configuration ─── */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} />
@@ -253,7 +258,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View> </View>
</View> </View>
{/* ─── Booth Behavior ─── */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
@@ -304,7 +308,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View> </View>
</View> </View>
{/* ─── Continuous Shooting ─── */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
@@ -337,7 +340,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View> </View>
</View> </View>
{/* ─── Photo Frames ─── */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
@@ -414,7 +416,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
)} )}
</View> </View>
{/* ─── Date/Time Overlay ─── */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} />
@@ -433,7 +434,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
)} )}
</View> </View>
{/* ─── Kiosk Mode ─── */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} /> <FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} />
@@ -474,7 +474,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View> </View>
</View> </View>
{/* ─── Save Button ─── */}
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}> <TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
<LinearGradient <LinearGradient
colors={THEME.gradient.primary} colors={THEME.gradient.primary}
@@ -487,6 +486,32 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
{/* Log Modal */}
<Modal visible={logs !== null} transparent animationType="slide">
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', justifyContent: 'center', alignItems: 'center' }}>
<View style={{ width: '90%', height: '85%', backgroundColor: '#222', borderRadius: 10, padding: 20 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold', color: '#fff', marginBottom: 10 }}>App Logs</Text>
<ScrollView style={{ flex: 1, backgroundColor: '#000', padding: 10, borderRadius: 5 }}>
<Text style={{ color: '#0f0', fontFamily: 'monospace' }}>{logs}</Text>
</ScrollView>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 15 }}>
<TouchableOpacity
style={{ backgroundColor: THEME.colors.primary, padding: 15, borderRadius: 8 }}
onPress={() => setLogs(null)}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Schließen</Text>
</TouchableOpacity>
<TouchableOpacity
style={{ backgroundColor: THEME.colors.danger, padding: 15, borderRadius: 8 }}
onPress={async () => { await logger.clearLogs(); setLogs(await logger.readLogs()); }}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Leeren</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</Animated.View> </Animated.View>
); );
} }
@@ -707,4 +732,11 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
letterSpacing: 2, letterSpacing: 2,
}, },
logBtn: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 8,
},
}); });
+8
View File
@@ -23,6 +23,7 @@ import {
import { THEME } from '../styles/theme'; import { THEME } from '../styles/theme';
import { printImageLocal } from '../services/printer'; import { printImageLocal } from '../services/printer';
import { saveToGallery } from '../services/storage'; import { saveToGallery } from '../services/storage';
import { logger } from '../services/logger';
import DraggableSticker, { StickerData } from '../components/DraggableSticker'; import DraggableSticker, { StickerData } from '../components/DraggableSticker';
import StickerTray from '../components/StickerTray'; import StickerTray from '../components/StickerTray';
import FrameOverlay from '../components/FrameOverlay'; import FrameOverlay from '../components/FrameOverlay';
@@ -70,6 +71,13 @@ export default function PreviewScreen({
const [selectedStickerId, setSelectedStickerId] = useState<string | null>(null); const [selectedStickerId, setSelectedStickerId] = useState<string | null>(null);
const [showStickerTray, setShowStickerTray] = useState<boolean>(false); const [showStickerTray, setShowStickerTray] = useState<boolean>(false);
React.useEffect(() => {
logger.log(`PreviewScreen mounted with ${photoUris.length} photos. currentPhoto: ${photoUris[photoUris.length - 1]}`);
return () => {
logger.log('PreviewScreen unmounting.');
};
}, [photoUris]);
// Idle Timer state // Idle Timer state
const idleTimerRef = useRef<any>(null); const idleTimerRef = useRef<any>(null);
+82
View File
@@ -0,0 +1,82 @@
import * as FileSystem from 'expo-file-system';
const logFileUri = FileSystem.documentDirectory + 'app_logs.txt';
export const logger = {
log: async (message: string) => {
try {
const timestamp = new Date().toISOString();
const logLine = `[INFO] ${timestamp}: ${message}\n`;
console.log(logLine.trim());
const fileInfo = await FileSystem.getInfoAsync(logFileUri);
if (!fileInfo.exists) {
await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 });
} else {
// Read existing and append (simplistic approach for small logs)
// expo-file-system doesn't have an append method out of the box in older versions,
// but wait, we can just read, then write. To avoid OOM we should truncate if too big.
const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
// Keep last 100000 chars
let newContent = current + logLine;
if (newContent.length > 100000) {
newContent = newContent.slice(-100000);
}
await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 });
}
} catch (e) {
console.log('Failed to write log:', e);
}
},
error: async (message: string, error?: any) => {
try {
const timestamp = new Date().toISOString();
let errorString = '';
if (error) {
if (error instanceof Error) {
errorString = `\n Name: ${error.name}\n Message: ${error.message}\n Stack: ${error.stack}`;
} else if (typeof error === 'object') {
errorString = `\n Object: ${JSON.stringify(error)}`;
} else {
errorString = `\n Value: ${String(error)}`;
}
}
const logLine = `[ERROR] ${timestamp}: ${message}${errorString}\n`;
console.error(logLine.trim());
const fileInfo = await FileSystem.getInfoAsync(logFileUri);
if (!fileInfo.exists) {
await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 });
} else {
const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
let newContent = current + logLine;
if (newContent.length > 100000) {
newContent = newContent.slice(-100000);
}
await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 });
}
} catch (e) {
console.log('Failed to write error log:', e);
}
},
readLogs: async (): Promise<string> => {
try {
const fileInfo = await FileSystem.getInfoAsync(logFileUri);
if (!fileInfo.exists) return 'Keine Logs vorhanden.';
return await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
} catch (e) {
return `Fehler beim Lesen der Logs: ${e}`;
}
},
clearLogs: async () => {
try {
await FileSystem.deleteAsync(logFileUri, { idempotent: true });
} catch (e) {
console.log('Failed to clear logs:', e);
}
}
};