2026-08-23 00:35:51 +00:00

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

cd pulse-clock
npm install

Copy the example environment file and configure:

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

npm run dev

Open http://localhost:3000 to access the application.

Build for Production

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:

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

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:

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:

openssl rand -base64 32

For local Docker testing:

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.

S
Description
pulsy — Pulse Clock for pulsy.hellobaka.com
Readme
160 KiB
Languages
TypeScript 96.8%
JavaScript 2.5%
Dockerfile 0.7%