589 lines
18 KiB
TypeScript
589 lines
18 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
SafeAreaView,
|
|
Animated,
|
|
Modal,
|
|
} from 'react-native';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
|
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
|
import { SystemStatus, KillSwitchLog } from '../types';
|
|
import VaultScreen from './VaultScreen';
|
|
|
|
// Animation timing constants
|
|
const ANIMATION_DURATION = {
|
|
pulse: 1200,
|
|
glow: 1500,
|
|
rotate: 30000,
|
|
heartbeatPress: 150,
|
|
} as const;
|
|
|
|
// Icon names type for type safety
|
|
type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle';
|
|
|
|
// Status configuration with nautical theme
|
|
const statusConfig: Record<SystemStatus, {
|
|
color: string;
|
|
label: string;
|
|
icon: StatusIconName;
|
|
description: string;
|
|
gradientColors: [string, string];
|
|
}> = {
|
|
normal: {
|
|
color: colors.sentinel.statusNormal,
|
|
label: 'ALL CLEAR',
|
|
icon: 'checkmark-circle',
|
|
description: 'The lighthouse burns bright. All systems nominal.',
|
|
gradientColors: ['#6BBF8A', '#4A9F6A'],
|
|
},
|
|
warning: {
|
|
color: colors.sentinel.statusWarning,
|
|
label: 'STORM WARNING',
|
|
icon: 'warning',
|
|
description: 'Anomaly detected. Captain\'s attention required.',
|
|
gradientColors: ['#E5B873', '#C99953'],
|
|
},
|
|
releasing: {
|
|
color: colors.sentinel.statusCritical,
|
|
label: 'RELEASE ACTIVE',
|
|
icon: 'alert-circle',
|
|
description: 'Legacy release protocol initiated.',
|
|
gradientColors: ['#E57373', '#C55353'],
|
|
},
|
|
};
|
|
|
|
// Mock data
|
|
const initialLogs: KillSwitchLog[] = [
|
|
{ id: '1', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-18T09:30:00') },
|
|
{ id: '2', action: 'SUBSCRIPTION_VERIFIED', timestamp: new Date('2024-01-17T00:00:00') },
|
|
{ id: '3', action: 'JOURNAL_ACTIVITY', timestamp: new Date('2024-01-16T15:42:00') },
|
|
{ id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') },
|
|
];
|
|
|
|
export default function SentinelScreen() {
|
|
const [status, setStatus] = useState<SystemStatus>('normal');
|
|
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));
|
|
const [lastFlowActivity] = useState(new Date('2024-01-18T10:30:00'));
|
|
const [logs, setLogs] = useState<KillSwitchLog[]>(initialLogs);
|
|
const [pulseAnim] = useState(new Animated.Value(1));
|
|
const [glowAnim] = useState(new Animated.Value(0.5));
|
|
const [rotateAnim] = useState(new Animated.Value(0));
|
|
const [showVault, setShowVault] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const pulseAnimation = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1.06,
|
|
duration: ANIMATION_DURATION.pulse,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1,
|
|
duration: ANIMATION_DURATION.pulse,
|
|
useNativeDriver: true,
|
|
}),
|
|
])
|
|
);
|
|
pulseAnimation.start();
|
|
|
|
const glowAnimation = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(glowAnim, {
|
|
toValue: 1,
|
|
duration: ANIMATION_DURATION.glow,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(glowAnim, {
|
|
toValue: 0.5,
|
|
duration: ANIMATION_DURATION.glow,
|
|
useNativeDriver: true,
|
|
}),
|
|
])
|
|
);
|
|
glowAnimation.start();
|
|
|
|
const rotateAnimation = Animated.loop(
|
|
Animated.timing(rotateAnim, {
|
|
toValue: 1,
|
|
duration: ANIMATION_DURATION.rotate,
|
|
useNativeDriver: true,
|
|
})
|
|
);
|
|
rotateAnimation.start();
|
|
|
|
return () => {
|
|
pulseAnimation.stop();
|
|
glowAnimation.stop();
|
|
rotateAnimation.stop();
|
|
};
|
|
}, [pulseAnim, glowAnim, rotateAnim]);
|
|
|
|
const openVault = () => setShowVault(true);
|
|
|
|
const handleHeartbeat = () => {
|
|
Animated.sequence([
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1.15,
|
|
duration: ANIMATION_DURATION.heartbeatPress,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1,
|
|
duration: ANIMATION_DURATION.heartbeatPress,
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start();
|
|
|
|
const newLog: KillSwitchLog = {
|
|
id: Date.now().toString(),
|
|
action: 'HEARTBEAT_CONFIRMED',
|
|
timestamp: new Date(),
|
|
};
|
|
setLogs((prevLogs) => [newLog, ...prevLogs]);
|
|
|
|
if (status === 'warning') {
|
|
setStatus('normal');
|
|
}
|
|
};
|
|
|
|
const formatDateTime = (date: Date) =>
|
|
date.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
|
|
const formatTimeAgo = (date: Date) => {
|
|
const now = new Date();
|
|
const diff = now.getTime() - date.getTime();
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
if (hours > 24) return `${Math.floor(hours / 24)} days ago`;
|
|
if (hours > 0) return `${hours}h ${minutes}m ago`;
|
|
return `${minutes}m ago`;
|
|
};
|
|
|
|
const currentStatus = statusConfig[status];
|
|
const spin = rotateAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ['0deg', '360deg'],
|
|
});
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<LinearGradient
|
|
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
|
|
style={styles.gradient}
|
|
>
|
|
<SafeAreaView style={styles.safeArea}>
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={styles.scrollContent}
|
|
>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View style={styles.headerTitleRow}>
|
|
<FontAwesome5 name="anchor" size={24} color={colors.sentinel.primary} />
|
|
<Text style={styles.title}>LIGHTHOUSE</Text>
|
|
</View>
|
|
<Text style={styles.subtitle}>The Watchful Guardian</Text>
|
|
</View>
|
|
|
|
{/* Status Display */}
|
|
<View style={styles.statusContainer}>
|
|
<Animated.View
|
|
style={[
|
|
styles.statusCircleOuter,
|
|
{
|
|
transform: [{ scale: pulseAnim }],
|
|
opacity: glowAnim,
|
|
backgroundColor: `${currentStatus.color}20`,
|
|
},
|
|
]}
|
|
/>
|
|
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
|
<LinearGradient
|
|
colors={currentStatus.gradientColors}
|
|
style={styles.statusCircle}
|
|
>
|
|
<Ionicons name={currentStatus.icon} size={56} color="#fff" />
|
|
</LinearGradient>
|
|
</Animated.View>
|
|
<Text style={[styles.statusLabel, { color: currentStatus.color }]}>
|
|
{currentStatus.label}
|
|
</Text>
|
|
<Text style={styles.statusDescription}>
|
|
{currentStatus.description}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Ship Wheel Watermark */}
|
|
<View style={styles.wheelWatermark}>
|
|
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
|
<MaterialCommunityIcons
|
|
name="ship-wheel"
|
|
size={200}
|
|
color={colors.sentinel.primary}
|
|
style={{ opacity: 0.03 }}
|
|
/>
|
|
</Animated.View>
|
|
</View>
|
|
|
|
{/* Metrics Grid */}
|
|
<View style={styles.metricsGrid}>
|
|
<View style={styles.metricCard}>
|
|
<View style={styles.metricIconContainer}>
|
|
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
|
|
</View>
|
|
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
|
|
<Text style={styles.metricValue}>{formatTimeAgo(lastSubscriptionCheck)}</Text>
|
|
<Text style={styles.metricTime}>{formatDateTime(lastSubscriptionCheck)}</Text>
|
|
</View>
|
|
<View style={styles.metricCard}>
|
|
<View style={styles.metricIconContainer}>
|
|
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
|
|
</View>
|
|
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
|
|
<Text style={styles.metricValue}>{formatTimeAgo(lastFlowActivity)}</Text>
|
|
<Text style={styles.metricTime}>{formatDateTime(lastFlowActivity)}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Shadow Vault Access */}
|
|
<View style={styles.vaultAccessCard}>
|
|
<View style={styles.vaultAccessIcon}>
|
|
<MaterialCommunityIcons name="treasure-chest" size={22} color={colors.nautical.teal} />
|
|
</View>
|
|
<View style={styles.vaultAccessContent}>
|
|
<Text style={styles.vaultAccessTitle}>Shadow Vault</Text>
|
|
<Text style={styles.vaultAccessText}>
|
|
Access sealed assets from the Lighthouse.
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={styles.vaultAccessButton}
|
|
onPress={openVault}
|
|
activeOpacity={0.8}
|
|
accessibilityLabel="Open Shadow Vault"
|
|
accessibilityRole="button"
|
|
>
|
|
<Text style={styles.vaultAccessButtonText}>Open</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Heartbeat Button */}
|
|
<TouchableOpacity
|
|
style={styles.heartbeatButton}
|
|
onPress={handleHeartbeat}
|
|
activeOpacity={0.9}
|
|
accessibilityLabel="Signal the watch - Confirm your presence"
|
|
accessibilityRole="button"
|
|
>
|
|
<LinearGradient
|
|
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
|
style={styles.heartbeatGradient}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
>
|
|
<View style={styles.heartbeatContent}>
|
|
<MaterialCommunityIcons name="lighthouse" size={32} color="#fff" />
|
|
<View>
|
|
<Text style={styles.heartbeatText}>SIGNAL THE WATCH</Text>
|
|
<Text style={styles.heartbeatSubtext}>Confirm your presence, Captain</Text>
|
|
</View>
|
|
</View>
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
|
|
{/* Watch Log */}
|
|
<View style={styles.logsSection}>
|
|
<View style={styles.logsSectionHeader}>
|
|
<Feather name="activity" size={18} color={colors.sentinel.primary} />
|
|
<Text style={styles.logsSectionTitle}>WATCH LOG</Text>
|
|
</View>
|
|
{logs.map((log) => (
|
|
<View key={log.id} style={styles.logItem}>
|
|
<View style={styles.logDot} />
|
|
<View style={styles.logContent}>
|
|
<Text style={styles.logAction}>{log.action}</Text>
|
|
<Text style={styles.logTime}>{formatDateTime(log.timestamp)}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
</LinearGradient>
|
|
|
|
{/* Vault Modal */}
|
|
<Modal
|
|
visible={showVault}
|
|
animationType="slide"
|
|
onRequestClose={() => setShowVault(false)}
|
|
>
|
|
<View style={styles.vaultModalContainer}>
|
|
{showVault ? <VaultScreen /> : null}
|
|
<TouchableOpacity
|
|
style={styles.vaultCloseButton}
|
|
onPress={() => setShowVault(false)}
|
|
activeOpacity={0.85}
|
|
accessibilityLabel="Close vault"
|
|
accessibilityRole="button"
|
|
>
|
|
<Ionicons name="close" size={20} color={colors.nautical.cream} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</Modal>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1 },
|
|
gradient: { flex: 1 },
|
|
safeArea: { flex: 1 },
|
|
scrollView: { flex: 1 },
|
|
scrollContent: {
|
|
padding: spacing.lg,
|
|
paddingBottom: 120,
|
|
},
|
|
header: { marginBottom: spacing.xl },
|
|
headerTitleRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing.sm,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
title: {
|
|
fontSize: typography.fontSize.xl,
|
|
fontWeight: '700',
|
|
color: colors.sentinel.text,
|
|
letterSpacing: typography.letterSpacing.widest,
|
|
fontFamily: typography.fontFamily.serif,
|
|
},
|
|
subtitle: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.sentinel.textSecondary,
|
|
marginLeft: spacing.xl + spacing.sm,
|
|
fontStyle: 'italic',
|
|
},
|
|
statusContainer: {
|
|
alignItems: 'center',
|
|
paddingVertical: spacing.xl,
|
|
marginBottom: spacing.lg,
|
|
position: 'relative',
|
|
},
|
|
statusCircleOuter: {
|
|
position: 'absolute',
|
|
width: 170,
|
|
height: 170,
|
|
borderRadius: 85,
|
|
},
|
|
statusCircle: {
|
|
width: 140,
|
|
height: 140,
|
|
borderRadius: 70,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: spacing.md,
|
|
...shadows.glow,
|
|
},
|
|
statusLabel: {
|
|
fontSize: typography.fontSize.xl,
|
|
fontWeight: '700',
|
|
letterSpacing: typography.letterSpacing.widest,
|
|
marginBottom: spacing.sm,
|
|
},
|
|
statusDescription: {
|
|
fontSize: typography.fontSize.base,
|
|
color: colors.sentinel.textSecondary,
|
|
textAlign: 'center',
|
|
maxWidth: 280,
|
|
fontStyle: 'italic',
|
|
},
|
|
wheelWatermark: {
|
|
position: 'absolute',
|
|
top: 200,
|
|
right: -60,
|
|
opacity: 0.5,
|
|
},
|
|
metricsGrid: {
|
|
flexDirection: 'row',
|
|
gap: spacing.md,
|
|
marginBottom: spacing.lg,
|
|
},
|
|
metricCard: {
|
|
flex: 1,
|
|
backgroundColor: colors.sentinel.cardBackground,
|
|
borderRadius: borderRadius.xl,
|
|
padding: spacing.base,
|
|
borderWidth: 1,
|
|
borderColor: colors.sentinel.cardBorder,
|
|
},
|
|
metricIconContainer: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
backgroundColor: `${colors.sentinel.primary}15`,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: spacing.sm,
|
|
},
|
|
metricLabel: {
|
|
fontSize: typography.fontSize.xs,
|
|
color: colors.sentinel.textSecondary,
|
|
letterSpacing: typography.letterSpacing.wide,
|
|
marginBottom: spacing.xs,
|
|
fontWeight: '600',
|
|
},
|
|
metricValue: {
|
|
fontSize: typography.fontSize.md,
|
|
color: colors.sentinel.text,
|
|
fontWeight: '700',
|
|
marginBottom: spacing.xs,
|
|
},
|
|
metricTime: {
|
|
fontSize: typography.fontSize.xs,
|
|
color: colors.sentinel.textSecondary,
|
|
fontFamily: typography.fontFamily.mono,
|
|
},
|
|
heartbeatButton: {
|
|
borderRadius: borderRadius.xl,
|
|
overflow: 'hidden',
|
|
marginBottom: spacing.xl,
|
|
...shadows.medium,
|
|
},
|
|
heartbeatGradient: { padding: spacing.lg },
|
|
heartbeatContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: spacing.md,
|
|
},
|
|
heartbeatText: {
|
|
fontSize: typography.fontSize.lg,
|
|
fontWeight: '700',
|
|
color: '#fff',
|
|
letterSpacing: typography.letterSpacing.wider,
|
|
},
|
|
heartbeatSubtext: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: 'rgba(255, 255, 255, 0.8)',
|
|
marginTop: 2,
|
|
fontStyle: 'italic',
|
|
},
|
|
logsSection: {
|
|
backgroundColor: colors.sentinel.cardBackground,
|
|
borderRadius: borderRadius.xl,
|
|
padding: spacing.base,
|
|
borderWidth: 1,
|
|
borderColor: colors.sentinel.cardBorder,
|
|
},
|
|
logsSectionHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing.sm,
|
|
marginBottom: spacing.md,
|
|
paddingBottom: spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.sentinel.cardBorder,
|
|
},
|
|
logsSectionTitle: {
|
|
fontSize: typography.fontSize.xs,
|
|
color: colors.sentinel.textSecondary,
|
|
letterSpacing: typography.letterSpacing.widest,
|
|
fontWeight: '700',
|
|
},
|
|
logItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
paddingVertical: spacing.sm,
|
|
},
|
|
logDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: colors.sentinel.primary,
|
|
marginTop: 6,
|
|
marginRight: spacing.md,
|
|
},
|
|
logContent: { flex: 1 },
|
|
logAction: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.sentinel.text,
|
|
fontFamily: typography.fontFamily.mono,
|
|
fontWeight: '500',
|
|
marginBottom: 2,
|
|
},
|
|
logTime: {
|
|
fontSize: typography.fontSize.xs,
|
|
color: colors.sentinel.textSecondary,
|
|
fontFamily: typography.fontFamily.mono,
|
|
},
|
|
vaultAccessCard: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.sentinel.cardBackground,
|
|
borderRadius: borderRadius.xl,
|
|
padding: spacing.base,
|
|
marginBottom: spacing.lg,
|
|
borderWidth: 1,
|
|
borderColor: colors.sentinel.cardBorder,
|
|
},
|
|
vaultAccessIcon: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
backgroundColor: `${colors.nautical.teal}20`,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginRight: spacing.md,
|
|
},
|
|
vaultAccessContent: { flex: 1 },
|
|
vaultAccessTitle: {
|
|
fontSize: typography.fontSize.base,
|
|
fontWeight: '600',
|
|
color: colors.sentinel.text,
|
|
marginBottom: 2,
|
|
},
|
|
vaultAccessText: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.sentinel.textSecondary,
|
|
},
|
|
vaultAccessButton: {
|
|
backgroundColor: colors.nautical.teal,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: borderRadius.full,
|
|
},
|
|
vaultAccessButtonText: {
|
|
color: colors.nautical.cream,
|
|
fontWeight: '700',
|
|
fontSize: typography.fontSize.sm,
|
|
},
|
|
vaultModalContainer: {
|
|
flex: 1,
|
|
backgroundColor: colors.vault.background,
|
|
},
|
|
vaultCloseButton: {
|
|
position: 'absolute',
|
|
top: spacing.xl + spacing.lg,
|
|
right: spacing.lg,
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
backgroundColor: 'rgba(26, 58, 74, 0.65)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
});
|