frontend first version

This commit is contained in:
Ada
2026-01-20 21:11:04 -08:00
commit 7944b9f7ed
29 changed files with 16468 additions and 0 deletions

View File

@@ -0,0 +1,516 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
SafeAreaView,
Animated,
} 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';
// Status configuration with nautical theme
const statusConfig: Record<SystemStatus, {
color: string;
label: string;
icon: string;
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));
useEffect(() => {
// Pulse animation
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.06,
duration: 1200,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1200,
useNativeDriver: true,
}),
])
).start();
// Glow animation
Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, {
toValue: 1,
duration: 1500,
useNativeDriver: true,
}),
Animated.timing(glowAnim, {
toValue: 0.5,
duration: 1500,
useNativeDriver: true,
}),
])
).start();
// Slow rotate for ship wheel
Animated.loop(
Animated.timing(rotateAnim, {
toValue: 1,
duration: 30000,
useNativeDriver: true,
})
).start();
}, []);
const handleHeartbeat = () => {
// Animate pulse
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.15,
duration: 150,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 150,
useNativeDriver: true,
}),
]).start();
// Add new log
const newLog: KillSwitchLog = {
id: Date.now().toString(),
action: 'HEARTBEAT_CONFIRMED',
timestamp: new Date(),
};
setLogs([newLog, ...logs]);
// Reset status if warning
if (status === 'warning') {
setStatus('normal');
}
};
const formatDateTime = (date: Date) => {
return 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) {
const days = Math.floor(hours / 24);
return `${days} 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 as any} 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>
{/* Heartbeat Button */}
<TouchableOpacity
style={styles.heartbeatButton}
onPress={handleHeartbeat}
activeOpacity={0.9}
>
<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>
</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,
},
});