Initial commit: EatMe attendance tracker

Google-SSO PWA for bistro employee clock-in/out, admin employee
management, and stats with CSV export. Express + SQLite backend,
React + Zustand frontend in the light-mono-tui design language.
Multi-stage Dockerfile, compose.yaml for image-based deploys, nginx
reverse-proxy template, and an OKF documentation bundle in docs/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Michal Pemcak
2026-08-12 11:57:55 +02:00
commit fffcb73ea4
90 changed files with 3411 additions and 0 deletions

71
docs/api/admin-routes.md Normal file
View File

@@ -0,0 +1,71 @@
---
type: API Endpoint
title: Admin routes
description: Employee CRUD (add/soft-delete), org-wide and per-employee stats, and CSV export — all requireAdmin.
resource: backend/src/routes/admin.ts
tags: [api, admin]
timestamp: 2026-08-12T00:00:00Z
---
# Admin routes
Mounted at `/api/admin`. Every route requires `requireAuth` + `requireAdmin`
(role resolved from `ADMIN_EMAILS`, see [Auth flow](/docs/architecture/auth-flow.md)).
# Examples
```
GET /api/admin/employees
200 -> { "employees": Employee[] } // includes inactive (soft-deleted) rows
```
```
POST /api/admin/employees
Body: { "email": string, "name"?: string }
201 -> { "employee": Employee }
```
Upserts by email — see [employees](/docs/data-model/employees.md) for the
reactivate-on-re-add behavior.
```
DELETE /api/admin/employees/:id
204 on success, 404 if not found.
```
Soft delete only (`active = 0`) — never removes the row or its attendance
history.
```
GET /api/admin/stats?month=YYYY-MM
200 -> {
"range": { "from": string, "to": string },
"totals": { "totalWorkedMs": number, "totalBreakMs": number, "shiftCount": number },
"employees": [{ "employee": Employee, "totalWorkedMs": number, "totalBreakMs": number, "shiftCount": number }]
}
```
Same calendar-month default as the employee's own stats endpoint.
```
GET /api/admin/stats/:id?month=YYYY-MM
200 -> { "employee": Employee, "range": {...}, "sessions": Session[], "totalWorkedMs": number, "totalBreakMs": number, "shiftCount": number }
404 -> employee not found
```
```
GET /api/admin/stats/:id/export?month=YYYY-MM
200, Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="<email>_<YYYY-MM>.csv"
```
Columns: `Datum, Příchod, Odchod, Pauza (h:mm), Odpracováno (h:mm)`, one row
per shift, sorted chronologically (`backend/src/util/csv.ts`). Prefixed with
a UTF-8 BOM so Excel renders the Czech diacritics correctly. Downloaded from
the frontend as a plain `<a href=... download>` — the session cookie rides
along automatically since it's a same-origin GET.
# Related
- [employees](/docs/data-model/employees.md)
- [attendance_events](/docs/data-model/attendance-events.md)
- [Attendance routes](./attendance-routes.md)
- [Frontend stores](/docs/frontend/stores.md) — `adminStore` owns the
selected month and drives both the summary table and the per-employee
chart off it

View File

@@ -0,0 +1,54 @@
---
type: API Endpoint
title: Attendance routes
description: POST /api/attendance/event, GET /api/attendance/state, GET /api/attendance/me — the employee's own clock in/out and stats.
resource: backend/src/routes/attendance.ts
tags: [api, attendance]
timestamp: 2026-08-12T00:00:00Z
---
# Attendance routes
Mounted at `/api/attendance`. Every route requires `requireAuth` +
`requireEmployee` — admins with no employee row get `403` here, by design
(the owner isn't necessarily clocking in themselves).
# Examples
```
POST /api/attendance/event
Body: { "type": "clock_in" | "clock_out" | "break_start" | "break_end" }
201 -> { "event": AttendanceEvent, "status": LiveStatus }
400 -> invalid type
409 -> invalid transition for the current status, e.g. break_start while
already clocked_out — { "error": string, "status": LiveStatus }
```
```
GET /api/attendance/state
200 -> { "status": "clocked_out" | "working" | "on_break" }
```
```
GET /api/attendance/me?month=YYYY-MM (or ?from=YYYY-MM-DD&to=YYYY-MM-DD)
Defaults to the current calendar month if no query params are given
— see parseRange in backend/src/util/dateRange.ts.
200 -> {
"range": { "from": string, "to": string },
"sessions": Session[],
"totalWorkedMs": number,
"totalBreakMs": number,
"shiftCount": number
}
```
`Session` and the state machine behind these responses are documented at
[attendance_events](/docs/data-model/attendance-events.md).
# Related
- [attendance_events](/docs/data-model/attendance-events.md)
- [Admin routes](./admin-routes.md) — same stats shape, but for any employee
- [Frontend stores](/docs/frontend/stores.md) — `attendanceStore` polls
`GET /state` + `GET /me` every 60s and ticks a local clock every second
between polls

51
docs/api/auth-routes.md Normal file
View File

@@ -0,0 +1,51 @@
---
type: API Endpoint
title: Auth routes
description: POST /api/auth/google, POST /api/auth/logout, GET /api/auth/me.
resource: backend/src/routes/auth.ts
tags: [api, auth]
timestamp: 2026-08-12T00:00:00Z
---
# Auth routes
Mounted at `/api/auth` (`backend/src/index.ts`).
# Examples
```
POST /api/auth/google
Body: { "credential": "<google id token>" }
200 -> { "user": SessionUser }
400 -> missing credential
401 -> invalid Google token
403 -> email not verified, or not an admin and not an active employee
```
Sets the `eatme_session` cookie on success. See
[Auth flow](/docs/architecture/auth-flow.md) for what happens before this
(role resolution) and what's inside the cookie.
```
POST /api/auth/logout
204, clears the session cookie. No auth required.
```
```
GET /api/auth/me
Requires a valid session cookie (requireAuth).
200 -> { "user": SessionUser }
401 -> not authenticated
```
`SessionUser` shape (`backend/src/types.ts`):
```ts
{ email: string; name: string | null; role: "admin" | "employee"; employeeId: number | null }
```
# Related
- [Auth flow](/docs/architecture/auth-flow.md)
- [Attendance routes](./attendance-routes.md)
- [Admin routes](./admin-routes.md)

8
docs/api/index.md Normal file
View File

@@ -0,0 +1,8 @@
# API
Express routers under `/api/*`, mounted in `backend/src/index.ts`. All
responses are JSON except the CSV export. Errors follow `{ "error": string }`.
- [Auth routes](./auth-routes.md) - `/api/auth/*` - Google sign-in, logout, current session
- [Attendance routes](./attendance-routes.md) - `/api/attendance/*` - the employee's own clock in/out and stats
- [Admin routes](./admin-routes.md) - `/api/admin/*` - employee management, org/individual stats, CSV export