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
+44 -12
View File
@@ -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>(String(currentSettings.countdownDuration));
const [printerIp, setPrinterIp] = useState<string>(currentSettings.printerIp);
const [password, setPassword] = useState<string>(currentSettings.adminPassword);
const [logs, setLogs] = useState<string | null>(null);
const [kioskActive, setKioskActive] = useState<boolean>(false);
const [isDeviceOwner, setIsDeviceOwner] = useState<boolean>(false);
// New settings state
const [frameMode, setFrameMode] = useState<FrameMode>(currentSettings.frameMode);
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId);
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 [welcomeText, setWelcomeText] = useState<string>(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 = <T extends string>(
options: { value: T; label: string }[],
current: T,
@@ -228,10 +227,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
</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>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* ─── Printer Configuration ─── */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} />
@@ -253,7 +258,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View>
</View>
{/* ─── Booth Behavior ─── */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
@@ -304,7 +308,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View>
</View>
{/* ─── Continuous Shooting ─── */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
@@ -337,7 +340,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View>
</View>
{/* ─── Photo Frames ─── */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
@@ -414,7 +416,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
)}
</View>
{/* ─── Date/Time Overlay ─── */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} />
@@ -433,7 +434,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
)}
</View>
{/* ─── Kiosk Mode ─── */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} />
@@ -474,7 +474,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View>
</View>
{/* ─── Save Button ─── */}
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
<LinearGradient
colors={THEME.gradient.primary}
@@ -487,6 +486,32 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</LinearGradient>
</TouchableOpacity>
</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>
);
}
@@ -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,
},
});
+8
View File
@@ -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<string | null>(null);
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
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);
}
}
};