Initial commit - DLB Gatekeeper project setup
This commit is contained in:
commit
596fbac864
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# DLB Gatekeeper
|
||||
|
||||
A Node.js application for controlling a gate via Raspberry Pi GPIO.
|
||||
|
||||
## Features
|
||||
|
||||
- REST API for gate control
|
||||
- Event logging
|
||||
- Configurable settings
|
||||
- GPIO control for relay
|
||||
- Simple web interface
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. Copy `.env.example` to `.env` and configure your settings:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
4. Build the TypeScript code:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
5. Start the server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `POST /api/trigger` - Trigger the gate
|
||||
- `GET /api/events` - Get recent gate events
|
||||
- `GET /api/settings` - Get current settings
|
||||
- `POST /api/settings` - Update settings
|
||||
|
||||
## Hardware Setup
|
||||
|
||||
- Connect relay control to GPIO pin (default: 18)
|
||||
- Ensure proper power supply for the relay
|
||||
- Ground connections as needed
|
||||
|
||||
## Development
|
||||
|
||||
Run in development mode with auto-reload:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- JWT authentication (to be implemented)
|
||||
- Rate limiting enabled
|
||||
- CORS protection
|
||||
- Helmet security headers
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DLB Gate Controller</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "dlb-gatekeeper-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"axios": "^1.6.2",
|
||||
"@heroicons/react": "^2.0.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { GateEvent, Settings } from './types';
|
||||
import { triggerGate, getRecentEvents, getSettings } from './api';
|
||||
|
||||
function App() {
|
||||
const [events, setEvents] = useState<GateEvent[]>([]);
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
const data = await getRecentEvents();
|
||||
setEvents(data);
|
||||
};
|
||||
|
||||
const fetchSettings = async () => {
|
||||
const data = await getSettings();
|
||||
setSettings(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
fetchSettings();
|
||||
|
||||
// Refresh events every 30 seconds
|
||||
const interval = setInterval(fetchEvents, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleTrigger = async (direction: 'open' | 'close') => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await triggerGate(direction);
|
||||
await fetchEvents(); // Refresh events after trigger
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
|
||||
<div className="relative py-3 sm:max-w-xl sm:mx-auto">
|
||||
<div className="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="divide-y divide-gray-200">
|
||||
<div className="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
|
||||
<h1 className="text-2xl font-bold mb-8">Gate Control</h1>
|
||||
|
||||
{/* Control Buttons */}
|
||||
<div className="flex space-x-4 mb-8">
|
||||
<button
|
||||
onClick={() => handleTrigger('open')}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 disabled:opacity-50"
|
||||
>
|
||||
Open Gate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTrigger('close')}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 disabled:opacity-50"
|
||||
>
|
||||
Close Gate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Recent Events */}
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-semibold mb-4">Recent Events</h2>
|
||||
<div className="space-y-2">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`p-2 rounded-md ${
|
||||
event.success ? 'bg-green-50' : 'bg-red-50'
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">
|
||||
<span className="font-semibold">{event.action}</span> at{' '}
|
||||
{formatTimestamp(event.timestamp)}
|
||||
{event.success ? ' ✓' : ' ✗'}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Display */}
|
||||
{settings && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-semibold mb-4">Settings</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>
|
||||
Max Open Time: {parseInt(settings.maxOpenTime) / 1000} seconds
|
||||
</p>
|
||||
<p>
|
||||
Trigger Duration: {parseInt(settings.triggerDuration)} ms
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import axios from 'axios';
|
||||
import { GateEvent, Settings } from './types';
|
||||
|
||||
// In development, Vite will proxy /api requests to the backend
|
||||
const API_URL = '/api';
|
||||
|
||||
export const triggerGate = async (direction: 'open' | 'close'): Promise<boolean> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/trigger`, { direction });
|
||||
return response.data.success;
|
||||
} catch (error) {
|
||||
console.error('Error triggering gate:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRecentEvents = async (): Promise<GateEvent[]> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/events`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getSettings = async (): Promise<Settings | null> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/settings`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSettings = async (settings: Partial<Settings>): Promise<boolean> => {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/settings`, settings);
|
||||
return response.data.success;
|
||||
} catch (error) {
|
||||
console.error('Error updating settings:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export interface GateEvent {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
action: string;
|
||||
source: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
maxOpenTime: string;
|
||||
triggerDuration: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "dlb-gatekeeper",
|
||||
"version": "1.0.0",
|
||||
"description": "Raspberry Pi Gate Controller",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev --respawn src/index.ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"onoff": "^6.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"sqlite3": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"typescript": "^5.3.3",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { gateController } from './hardware';
|
||||
import { logEvent } from './database';
|
||||
import { config } from './config';
|
||||
|
||||
class AutoCloseManager {
|
||||
private lastOpenTime: number | null = null;
|
||||
private intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
private readonly CHECK_INTERVAL = 60000; // 1 minute in milliseconds
|
||||
|
||||
constructor() {
|
||||
this.startChecking();
|
||||
}
|
||||
|
||||
public recordOpen(): void {
|
||||
this.lastOpenTime = Date.now();
|
||||
console.log('Gate opened, starting auto-close timer');
|
||||
}
|
||||
|
||||
private startChecking(): void {
|
||||
// Clear any existing interval
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
|
||||
// Start new interval
|
||||
this.intervalId = setInterval(() => {
|
||||
this.checkAndClose().catch(error => {
|
||||
console.error('Error in auto-close check:', error);
|
||||
});
|
||||
}, this.CHECK_INTERVAL);
|
||||
|
||||
console.log('Auto-close checker started');
|
||||
}
|
||||
|
||||
private async checkAndClose(): Promise<void> {
|
||||
if (!this.lastOpenTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeOpen = Date.now() - this.lastOpenTime;
|
||||
|
||||
if (timeOpen >= config.maxOpenTime) {
|
||||
console.log(`Gate has been open for ${timeOpen}ms, auto-closing`);
|
||||
|
||||
try {
|
||||
const success = await gateController.trigger();
|
||||
await logEvent('AUTO_CLOSE', 'system', success);
|
||||
|
||||
if (success) {
|
||||
this.lastOpenTime = null;
|
||||
console.log('Auto-close successful');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during auto-close:', error);
|
||||
await logEvent('AUTO_CLOSE_ERROR', 'system', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
console.log('Auto-close checker stopped');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const autoCloseManager = new AutoCloseManager();
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
// GPIO pin number for relay control
|
||||
relayPin: process.env.RELAY_PIN ? parseInt(process.env.RELAY_PIN) : 18,
|
||||
|
||||
// Duration to hold relay active (milliseconds)
|
||||
triggerDuration: process.env.TRIGGER_DURATION ? parseInt(process.env.TRIGGER_DURATION) : 500,
|
||||
|
||||
// Maximum time gate can be open (milliseconds)
|
||||
maxOpenTime: process.env.MAX_OPEN_TIME ? parseInt(process.env.MAX_OPEN_TIME) : 5 * 60 * 1000, // 5 minutes
|
||||
|
||||
// Database file location
|
||||
dbPath: process.env.DB_PATH || 'gatekeeper.db'
|
||||
};
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import sqlite3 from 'sqlite3';
|
||||
import { config } from './config';
|
||||
|
||||
interface EventRow {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
action: string;
|
||||
source: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface SettingRow {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const db = new sqlite3.Database(config.dbPath);
|
||||
|
||||
export async function initializeDatabase(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
// Create events table for logging
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
action TEXT NOT NULL,
|
||||
source TEXT,
|
||||
success BOOLEAN
|
||||
)
|
||||
`);
|
||||
|
||||
// Create settings table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function logEvent(action: string, source: string, success: boolean): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO events (action, source, success) VALUES (?, ?, ?)',
|
||||
[action, source, success],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRecentEvents(limit: number = 10): Promise<EventRow[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all<EventRow>(
|
||||
'SELECT * FROM events ORDER BY timestamp DESC LIMIT ?',
|
||||
[limit],
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSetting(key: string): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get<SettingRow>(
|
||||
'SELECT value FROM settings WHERE key = ?',
|
||||
[key],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row ? row.value : null);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function setSetting(key: string, value: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
|
||||
[key, value],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { Gpio } from 'onoff';
|
||||
import { config } from './config';
|
||||
|
||||
class GateController {
|
||||
private relay: Gpio;
|
||||
private isTriggering: boolean = false;
|
||||
|
||||
constructor() {
|
||||
// Initialize GPIO
|
||||
this.relay = new Gpio(config.relayPin, 'out');
|
||||
}
|
||||
|
||||
async trigger(): Promise<boolean> {
|
||||
if (this.isTriggering) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isTriggering = true;
|
||||
|
||||
// Activate relay
|
||||
await this.relay.write(1);
|
||||
|
||||
// Wait for trigger duration
|
||||
await new Promise(resolve => setTimeout(resolve, config.triggerDuration));
|
||||
|
||||
// Deactivate relay
|
||||
await this.relay.write(0);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error triggering gate:', error);
|
||||
return false;
|
||||
} finally {
|
||||
this.isTriggering = false;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await this.relay.unexport();
|
||||
}
|
||||
}
|
||||
|
||||
export const gateController = new GateController();
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { router } from './routes';
|
||||
import { initializeDatabase } from './database';
|
||||
import { config } from './config';
|
||||
import { autoCloseManager } from './autoClose';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per windowMs
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Routes
|
||||
app.use('/api', router);
|
||||
|
||||
// Serve static files for web interface
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Initialize database
|
||||
initializeDatabase().catch(console.error);
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down...');
|
||||
autoCloseManager.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import express from 'express';
|
||||
import { gateController } from './hardware';
|
||||
import { logEvent, getRecentEvents, getSetting, setSetting } from './database';
|
||||
import { autoCloseManager } from './autoClose';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
// Trigger gate
|
||||
router.post('/trigger', async (req, res) => {
|
||||
try {
|
||||
const success = await gateController.trigger();
|
||||
await logEvent('TRIGGER', req.ip || 'unknown', success);
|
||||
|
||||
// Record open time for auto-close
|
||||
if (success && req.body.direction === 'open') {
|
||||
autoCloseManager.recordOpen();
|
||||
}
|
||||
|
||||
res.json({ success });
|
||||
} catch (error) {
|
||||
console.error('Error triggering gate:', error);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get recent events
|
||||
router.get('/events', async (req, res) => {
|
||||
try {
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
|
||||
const events = await getRecentEvents(limit);
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get settings
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const settings = {
|
||||
maxOpenTime: await getSetting('maxOpenTime'),
|
||||
triggerDuration: await getSetting('triggerDuration')
|
||||
};
|
||||
res.json(settings);
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update settings
|
||||
router.post('/settings', async (req, res) => {
|
||||
try {
|
||||
const { maxOpenTime, triggerDuration } = req.body;
|
||||
|
||||
if (maxOpenTime) {
|
||||
await setSetting('maxOpenTime', maxOpenTime.toString());
|
||||
}
|
||||
|
||||
if (triggerDuration) {
|
||||
await setSetting('triggerDuration', triggerDuration.toString());
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error updating settings:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
Loading…
Reference in New Issue