# 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.