From 2ebb4a6f0151cb1c03ae8c9f0a79cf0ef4cf54cb Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Sun, 31 May 2026 14:44:48 +0200 Subject: [PATCH] Add on-device logger --- App.tsx | 22 ++++++++-- src/screens/AdminScreen.tsx | 56 +++++++++++++++++++----- src/screens/PreviewScreen.tsx | 8 ++++ src/services/logger.ts | 82 +++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 src/services/logger.ts diff --git a/App.tsx b/App.tsx index 4d892ee0..720b9801 100644 --- a/App.tsx +++ b/App.tsx @@ -4,7 +4,15 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import CameraScreen from './src/screens/CameraScreen'; import PreviewScreen from './src/screens/PreviewScreen'; 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'; type ScreenState = 'home' | 'camera' | 'preview' | 'admin'; @@ -42,9 +50,15 @@ export default function App() { setCurrentScreen('camera'); }; - const handlePhotoCaptured = (uri: string) => { - setPhotoUris((prev) => [...prev, uri]); - setCurrentScreen('preview'); + const handlePhotoCaptured = async (uri: string) => { + await logger.log(`Photo captured via CameraScreen: ${uri}`); + 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 diff --git a/src/screens/AdminScreen.tsx b/src/screens/AdminScreen.tsx index b2617d6b..1b1573d9 100644 --- a/src/screens/AdminScreen.tsx +++ b/src/screens/AdminScreen.tsx @@ -8,6 +8,7 @@ import { ScrollView, Switch, Alert, + Modal, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; @@ -22,9 +23,10 @@ import { faCalendarDay, faImages, faFloppyDisk, - faChampagneGlasses, + faFileLines, } from '@fortawesome/free-solid-svg-icons'; import { THEME } from '../styles/theme'; +import { logger } from '../services/logger'; import KioskMode from '../../modules/kiosk-mode'; import { AppSettings, saveSettings } from '../services/settings'; import { FRAMES } from '../data/frames'; @@ -63,11 +65,11 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS const [countdown, setCountdown] = useState(String(currentSettings.countdownDuration)); const [printerIp, setPrinterIp] = useState(currentSettings.printerIp); const [password, setPassword] = useState(currentSettings.adminPassword); + const [logs, setLogs] = useState(null); const [kioskActive, setKioskActive] = useState(false); const [isDeviceOwner, setIsDeviceOwner] = useState(false); - // New settings state const [frameMode, setFrameMode] = useState(currentSettings.frameMode); const [selectedFrameId, setSelectedFrameId] = useState(currentSettings.selectedFrameId); const [availableFrameIds, setAvailableFrameIds] = useState(currentSettings.availableFrameIds || []); @@ -78,7 +80,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS const [burstInterval, setBurstInterval] = useState(String(currentSettings.burstIntervalSec)); const [welcomeText, setWelcomeText] = useState(currentSettings.welcomeText); - // Load Kiosk and Device Owner status on mount useEffect(() => { const checkKioskStatus = () => { try { @@ -92,7 +93,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS }; checkKioskStatus(); - // Poll status every second const interval = setInterval(checkKioskStatus, 1000); return () => clearInterval(interval); }, []); @@ -196,7 +196,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS } }; - // Renders a 3-way toggle segment control const renderSegment = ( options: { value: T; label: string }[], current: T, @@ -228,10 +227,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS Zurück zur Fotobox + setLogs(await logger.readLogs())} + > + + Logs + - {/* ─── Printer Configuration ─── */} @@ -253,7 +258,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS - {/* ─── Booth Behavior ─── */} @@ -304,7 +308,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS - {/* ─── Continuous Shooting ─── */} @@ -337,7 +340,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS - {/* ─── Photo Frames ─── */} @@ -414,7 +416,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS )} - {/* ─── Date/Time Overlay ─── */} @@ -433,7 +434,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS )} - {/* ─── Kiosk Mode ─── */} @@ -474,7 +474,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS - {/* ─── Save Button ─── */} + + {/* Log Modal */} + + + + App Logs + + {logs} + + + setLogs(null)} + > + Schließen + + { await logger.clearLogs(); setLogs(await logger.readLogs()); }} + > + Leeren + + + + + ); } @@ -707,4 +732,11 @@ const styles = StyleSheet.create({ fontWeight: 'bold', letterSpacing: 2, }, + logBtn: { + flexDirection: 'row', + alignItems: 'center', + padding: 10, + backgroundColor: 'rgba(255,255,255,0.1)', + borderRadius: 8, + }, }); diff --git a/src/screens/PreviewScreen.tsx b/src/screens/PreviewScreen.tsx index f563e17e..2d3b7705 100644 --- a/src/screens/PreviewScreen.tsx +++ b/src/screens/PreviewScreen.tsx @@ -23,6 +23,7 @@ import { import { THEME } from '../styles/theme'; import { printImageLocal } from '../services/printer'; import { saveToGallery } from '../services/storage'; +import { logger } from '../services/logger'; import DraggableSticker, { StickerData } from '../components/DraggableSticker'; import StickerTray from '../components/StickerTray'; import FrameOverlay from '../components/FrameOverlay'; @@ -70,6 +71,13 @@ export default function PreviewScreen({ const [selectedStickerId, setSelectedStickerId] = useState(null); const [showStickerTray, setShowStickerTray] = useState(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 const idleTimerRef = useRef(null); diff --git a/src/services/logger.ts b/src/services/logger.ts new file mode 100644 index 00000000..41da7c34 --- /dev/null +++ b/src/services/logger.ts @@ -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 => { + 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); + } + } +};