initial: pulsy from pulse-clock-main

This commit is contained in:
Hermes
2026-08-23 00:35:51 +00:00
commit 023f0394b4
53 changed files with 11171 additions and 0 deletions
+317
View File
@@ -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.