initial: pulsy from pulse-clock-main
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
# Pulse Clock
|
||||
|
||||
Multi-tenant time clock and workforce management system for hotel/hospitality environments. Employees punch in/out and take breaks via a kiosk interface; administrators monitor time cards, detect scheduling issues, and export payroll data.
|
||||
|
||||
## Overview
|
||||
|
||||
`pulse-clock` provides:
|
||||
|
||||
- **Employee Kiosk**: Web-based punch interface with webcam capture
|
||||
- **Admin Dashboard**: Time grid view with issue detection
|
||||
- **Management**: Hotels and employees CRUD
|
||||
- **Payroll Export**: TSV download with validation checks
|
||||
- **Validation Engine**: State machine enforcing valid punch sequences
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** (v20.9 or higher)
|
||||
- **npm** (package manager)
|
||||
- **SQLite** (accessed through Drizzle and the libSQL client)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd pulse-clock
|
||||
npm install
|
||||
```
|
||||
|
||||
Copy the example environment file and configure:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your configuration. Key variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------- | ------------------------------------ | ----------------------- |
|
||||
| `PULSE_SECRET` | Login secret; use 32+ random chars | — |
|
||||
| `PULSE_RATE_LIMIT` | API rate limit (requests per window) | `60` |
|
||||
| `PULSE_RATE_WINDOW` | Rate limit window (seconds) | `60` |
|
||||
| `DATABASE_URL` | SQLite database path | `./data/pulse-clock.db` |
|
||||
|
||||
## Usage
|
||||
|
||||
### Development Server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) to access the application.
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Pulse Clock │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Kiosk Mode │ │ Admin Dashboard │ │
|
||||
│ │ /kiosk/* │ │ /admin/* │ │
|
||||
│ └────────┬────────┘ └────────┬────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ API Routes │ │
|
||||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||
│ │ │ /punch │ │ /time- │ │ /payroll │ │ │
|
||||
│ │ │ │ │ entries │ │ │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ time-entry-service.ts │ │
|
||||
│ │ (createValidatedTimeEntry) │ │
|
||||
│ │ Punch State Machine │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ SQLite Database │ │
|
||||
│ │ companies | employees | time_entries | audit_log │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Kiosk Punch Flow │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Employee Selects → Webcam Capture → Punch Button Click │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────┐ │
|
||||
│ │ POST /api/punch │ │
|
||||
│ │ { employeeId, type }│ │
|
||||
│ └────────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────┐ │
|
||||
│ │ Punch State Machine │ │
|
||||
│ │ validateNewEntry... │ │
|
||||
│ └────────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────┼──────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌──────────┐
|
||||
│ │ Valid Punch │ │ Invalid Punch │ │ Duplicate│
|
||||
│ │ → Save Entry │ │ → Reject │ │ → Ignore │
|
||||
│ └───────────────┘ └───────────────┘ └──────────┘
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Punch State Machine
|
||||
|
||||
```
|
||||
Valid Transitions:
|
||||
|
||||
OUT ──────▶ IN (clock in)
|
||||
IN ──────▶ OUT (clock out)
|
||||
IN ──────▶ BREAK_OUT (start break)
|
||||
BREAK_OUT ─▶ BREAK_IN (end break)
|
||||
|
||||
All other transitions are rejected unless forceOverride=true
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Employee Kiosk
|
||||
|
||||
- Live employee cards showing current status: **Off Duty** / **On Shift** / **On Break**
|
||||
- Webcam capture on each punch (base64 stored in DB)
|
||||
- Punch modal with Clock In, Clock Out, Start Break, End Break buttons
|
||||
- 5-second duplicate-punch guard on kiosk clicks
|
||||
- Auto-refreshes employee status every 5 seconds
|
||||
- Elapsed shift time shown on employee cards (net of break time)
|
||||
|
||||
### Admin Dashboard
|
||||
|
||||
- Date range selector with Today/Yesterday/Last 7/Last 14/Last 30 presets
|
||||
- Grid view: employees (rows) × calendar days (columns)
|
||||
- Red cell highlighting for validation issues
|
||||
- "Issues Only" filter toggle
|
||||
- Detail drawer per employee/day: full punch timeline
|
||||
- Timestamp editing through authenticated admin sessions
|
||||
- Add manual punch entries
|
||||
|
||||
### Validation Engine
|
||||
|
||||
Detects per employee per day:
|
||||
|
||||
- First entry is not Clock In
|
||||
- Consecutive duplicate entries (e.g., two INs in a row)
|
||||
- Break started without prior Clock In
|
||||
- Break ended without break start
|
||||
- Break started but never ended
|
||||
- Still clocked in at day end
|
||||
- Missing clock out
|
||||
|
||||
### Payroll Export
|
||||
|
||||
- Date range presets including Pay Period 1-15 and Pay Period 16-31
|
||||
- Preview table: Employee | Date | In | Out | Break (min) | Net Hours | Notes
|
||||
- Copy to clipboard (TSV) and Download .tsv buttons
|
||||
- Issues column flags validation problems per day row
|
||||
- Summary strip with total hours and error counts
|
||||
|
||||
### Public Hosting Security
|
||||
|
||||
- All pages redirect to `/login` unless the `pulse_auth` cookie is valid.
|
||||
- All API routes except `/api/authorize` and `/api/health` require the auth cookie.
|
||||
- `PULSE_SECRET` is required and must be at least 32 characters.
|
||||
- The auth cookie is `HttpOnly`, `SameSite=Lax`, `Secure` in production, and valid for 1 year.
|
||||
- Login attempts and authenticated API calls are rate limited by IP.
|
||||
- Mutating API requests require same-origin/CSRF protection.
|
||||
|
||||
### Multi-Tenancy
|
||||
|
||||
All data is scoped by `companyId`:
|
||||
|
||||
- Employees belong to a company
|
||||
- Time entries scoped to company + employee
|
||||
- Audit logs scoped to company
|
||||
|
||||
## Pages Reference
|
||||
|
||||
| Path | Description |
|
||||
| -------------------- | -------------------------------- |
|
||||
| `/` | Landing page with company grid |
|
||||
| `/login` | Secret-based login page |
|
||||
| `/kiosk` | Kiosk company selector |
|
||||
| `/kiosk/[companyId]` | Employee kiosk interface |
|
||||
| `/admin` | Admin time grid dashboard |
|
||||
| `/admin/manage` | Hotels and employees CRUD |
|
||||
| `/admin/payroll` | Payroll export with TSV download |
|
||||
|
||||
## API Reference
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| ---------------------------- | ------------------------ | ------------------------------ |
|
||||
| `/api/companies` | GET, POST | Company CRUD |
|
||||
| `/api/employees` | GET, POST | Employee CRUD |
|
||||
| `/api/employees/with-status` | GET | Employee list with live status |
|
||||
| `/api/punch` | POST | Kiosk punch (state machine) |
|
||||
| `/api/time-entries` | GET, POST, PATCH, DELETE | Time entry CRUD + manual edits |
|
||||
| `/api/payroll` | GET | Payroll TSV generation |
|
||||
| `/api/authorize` | GET, POST | Cookie auth check/login |
|
||||
| `/api/health` | GET | Unauthenticated healthcheck |
|
||||
|
||||
### Authentication
|
||||
|
||||
Open `/login` and enter `PULSE_SECRET`. Successful login sets the `pulse_auth` cookie.
|
||||
|
||||
API calls from the browser use that cookie automatically. For non-browser scripts, first authenticate and store the cookie:
|
||||
|
||||
```bash
|
||||
curl -c cookies.txt \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-pulse-csrf: 1" \
|
||||
-X POST https://localhost:3000/api/authorize \
|
||||
-d '{"secret":"your-32-character-minimum-secret"}'
|
||||
|
||||
curl -b cookies.txt \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-pulse-csrf: 1" \
|
||||
-X PATCH https://localhost:3000/api/time-entries \
|
||||
-d '{"id":123,"timestamp":1747507200}'
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
companies (id, name, created_at)
|
||||
employees (id, company_id, name, is_active, created_at)
|
||||
time_entries (id, employee_id, company_id, type, timestamp, photo_base64, is_deleted)
|
||||
audit_log (id, employee_id, company_id, admin_id, action, detail, created_at)
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pulse-clock/
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ │ ├── page.tsx # Landing page
|
||||
│ │ ├── kiosk/
|
||||
│ │ │ └── [companyId]/ # Kiosk UI
|
||||
│ │ ├── admin/
|
||||
│ │ │ ├── page.tsx # Time grid dashboard
|
||||
│ │ │ ├── manage/page.tsx # Hotels & employees CRUD
|
||||
│ │ │ └── payroll/page.tsx # Payroll export
|
||||
│ │ └── api/ # API routes
|
||||
│ ├── lib/ # Shared business logic
|
||||
│ │ ├── api.ts # fetchApi wrapper
|
||||
│ │ ├── validation.ts # State machine
|
||||
│ │ ├── calculations.ts # Time calculations
|
||||
│ │ ├── time.ts # Date utilities
|
||||
│ │ └── time-entry-service.ts
|
||||
│ ├── db/
|
||||
│ │ ├── schema.ts # Drizzle schema
|
||||
│ │ └── index.ts # Drizzle client and migrations
|
||||
│ └── components/ # React components
|
||||
├── drizzle/ # SQL migrations
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Docker / Coolify Deployment
|
||||
|
||||
The app includes a production `Dockerfile` for Coolify.
|
||||
|
||||
Recommended Coolify settings:
|
||||
|
||||
| Setting | Value |
|
||||
| ------------------- | ------------ |
|
||||
| Build Pack | Dockerfile |
|
||||
| Dockerfile Location | `Dockerfile` |
|
||||
| Port | `3000` |
|
||||
| Persistent Storage | `/app/data` |
|
||||
|
||||
Set these environment variables in Coolify:
|
||||
|
||||
```bash
|
||||
PULSE_SECRET=replace-with-at-least-32-random-characters
|
||||
DATABASE_URL=/app/data/pulse-clock.db
|
||||
PULSE_RATE_LIMIT=60
|
||||
PULSE_RATE_WINDOW=60
|
||||
```
|
||||
|
||||
Generate a deployment secret with:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
For local Docker testing:
|
||||
|
||||
```bash
|
||||
docker build -t pulse-clock .
|
||||
docker run --rm -p 3000:3000 -v pulse-clock-data:/app/data --env-file .env pulse-clock
|
||||
```
|
||||
|
||||
SQLite migrations run automatically on startup, and the database is stored in `/app/data` when deployed with the Docker defaults.
|
||||
Run one app replica per SQLite volume so startup migrations are not executed concurrently.
|
||||
Reference in New Issue
Block a user