dlbGatekeeper/frontend/src/App.tsx

239 lines
8.3 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { GateEvent, Settings, GateStatus } from './types';
import * as api from './api';
import { SettingsDialog } from './components/SettingsDialog';
import { Cog6ToothIcon } from '@heroicons/react/24/outline';
function App() {
const [events, setEvents] = useState<GateEvent[]>([]);
const [hasMoreEvents, setHasMoreEvents] = useState(false);
const [totalEvents, setTotalEvents] = useState(0);
const [settings, setSettings] = useState<Settings>({
maxOpenTimeSeconds: 300,
triggerDuration: 500,
mqtt: {
broker: 'localhost',
port: '1883',
username: '',
password: '',
clientId: 'gatekeeper',
enabled: false
},
gpio: {
gatePin: 17,
statusPin: 27
},
logging: {
level: 'WARNING',
maxBytes: 10 * 1024 * 1024, // 10MB
backupCount: 5
}
});
const [gateStatus, setGateStatus] = useState<GateStatus>({ isOpen: false, lastChanged: new Date().toISOString() });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const formatDate = (isoString: string) => {
const date = new Date(isoString);
return date.toLocaleString();
};
useEffect(() => {
const loadData = async () => {
try {
const [eventsData, settingsData, statusData] = await Promise.all([
api.getEvents(),
api.getSettings(),
api.getGateStatus()
]);
setEvents(eventsData.events);
setHasMoreEvents(eventsData.hasMore);
setTotalEvents(eventsData.total);
setSettings(settingsData);
setGateStatus(statusData);
} catch (err) {
setError('Failed to load data');
console.error(err);
}
};
const interval = setInterval(async () => {
try {
// Only update status and most recent events
const [statusData, eventsData] = await Promise.all([
api.getGateStatus(),
api.getEvents(10, 0)
]);
setGateStatus(statusData);
// Update only if we're on the first page and don't have more events loaded
if (events.length <= 10 && !hasMoreEvents) {
setEvents(eventsData.events);
setHasMoreEvents(eventsData.hasMore);
setTotalEvents(eventsData.total);
} else {
// If we have more events loaded, just prepend any new events
const newEvents = eventsData.events.filter(
newEvent => !events.some(
existingEvent =>
existingEvent.timestamp === newEvent.timestamp &&
existingEvent.action === newEvent.action
)
);
if (newEvents.length > 0) {
setEvents(prev => [...newEvents, ...prev]);
setTotalEvents(prev => prev + newEvents.length);
}
}
} catch (err) {
console.error('Failed to update status:', err);
}
}, 1000);
loadData();
return () => clearInterval(interval);
}, []);
const handleGateControl = async () => {
setLoading(true);
setError(null);
try {
const result = await api.triggerGate();
if (result.success) {
const newEvents = await api.getEvents();
setEvents(newEvents.events);
setGateStatus(prev => ({ ...prev, isOpen: result.isOpen }));
}
} catch (err) {
setError('Failed to trigger gate');
console.error(err);
} finally {
setLoading(false);
}
};
const handleSettingsSave = async () => {
setLoading(true);
setError(null);
try {
await api.updateSettings(settings);
setIsSettingsOpen(false);
} catch (err) {
setError('Failed to update settings');
console.error(err);
} finally {
setLoading(false);
}
};
const handleLoadMore = async () => {
try {
const moreEvents = await api.getEvents(10, events.length);
if (moreEvents.events.length > 0) {
setEvents(prev => [...prev, ...moreEvents.events]);
setHasMoreEvents(moreEvents.hasMore);
setTotalEvents(moreEvents.total);
}
} catch (err) {
setError('Failed to load more events');
console.error(err);
}
};
return (
<div className="min-h-screen bg-gray-100 py-2 flex flex-col justify-center">
<div className="relative sm:max-w-xl sm:mx-auto">
<div className="relative px-4 py-6 bg-white shadow-lg sm:rounded-3xl sm:p-12">
<div className="max-w-md mx-auto">
<div className="divide-y divide-gray-200">
<div className="py-4 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold text-gray-900">Gate Control</h1>
<button
onClick={() => setIsSettingsOpen(true)}
className="p-2 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<Cog6ToothIcon className="h-6 w-6" />
</button>
</div>
{/* Gate Status */}
<div className="mb-4">
<p className="text-lg font-semibold">
Status: <span className={gateStatus.isOpen ? "text-green-600" : "text-red-600"}>
{gateStatus.isOpen ? "Open" : "Closed"}
</span>
</p>
<p className="text-sm text-gray-500">
Last changed: {formatDate(gateStatus.lastChanged)}
</p>
</div>
{/* Gate Control Button */}
<div className="mt-4">
<button
onClick={handleGateControl}
disabled={loading}
className={`w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white
${loading ? 'bg-gray-400' : 'bg-blue-600 hover:bg-blue-700'}
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
>
{loading ? 'Processing...' : 'Trigger Gate'}
</button>
</div>
{/* Error Display */}
{error && (
<div className="mt-4 p-4 bg-red-100 text-red-700 rounded-md">
{error}
</div>
)}
{/* Recent Events */}
<div className="mt-6">
<h2 className="text-lg font-semibold mb-2">Recent Events</h2>
<div className="border rounded-lg divide-y divide-gray-200 max-h-96 overflow-y-auto">
{events.map((event, index) => (
<div key={index} className="px-3 py-2 hover:bg-gray-50 flex items-center justify-between text-sm">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{event.action}</span>
<span className="text-xs text-gray-500">({event.source})</span>
</div>
<div className="text-xs text-gray-500">{formatDate(event.timestamp)}</div>
</div>
<div className={`w-2 h-2 rounded-full ${event.success ? 'bg-green-500' : 'bg-red-500'}`} />
</div>
))}
</div>
{hasMoreEvents && (
<button
onClick={handleLoadMore}
className="mt-2 w-full text-sm text-gray-600 hover:text-gray-900 py-1 border rounded-md hover:bg-gray-50"
>
Show More ({events.length} of {totalEvents})
</button>
)}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Settings Dialog */}
<SettingsDialog
isOpen={isSettingsOpen}
onClose={() => {
setIsSettingsOpen(false);
// loadSettings(); // Refresh settings after dialog closes
}}
/>
</div>
);
}
export default App;