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:
45
docs/frontend/design-system.md
Normal file
45
docs/frontend/design-system.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
type: Design Language
|
||||
title: TUI monochrome design system
|
||||
description: How the light-mono-tui design language is implemented in this app's CSS.
|
||||
resource: frontend/src/styles/tui.css
|
||||
tags: [frontend, design, css]
|
||||
timestamp: 2026-08-12T00:00:00Z
|
||||
---
|
||||
|
||||
# TUI monochrome design system
|
||||
|
||||
The full design language spec lives outside this bundle at
|
||||
`~/doc/concepts/ui/light-mono-tui.md` (grey background, black type/borders,
|
||||
mono font, inverted active/hover states, no accent color, no radius, no
|
||||
shadow). This doc covers how EatMe implements it.
|
||||
|
||||
`frontend/src/styles/tui.css` defines the tokens (`--bg`, `--fg`, `--muted`,
|
||||
`--hover`, `--empty`, `--panel`, `--font`) and the component classes built on
|
||||
them: `.tabs`/`.tab`, `.btn`/`.btn-block`/`.btn-ghost`, `.panel`,
|
||||
`.field-row`, `.list`/`.row`, `.badge` (`-solid`, `-dashed`, `-double`),
|
||||
`.stat-grid`/`.stat-tile`, `.login-card`.
|
||||
|
||||
# App-specific additions
|
||||
|
||||
Built for this app, following the same rules (square edges, monochrome,
|
||||
1px borders) rather than introducing new visual language:
|
||||
|
||||
- **`.bar-chart-*`** (`frontend/src/components/BarChart.tsx`) — the
|
||||
per-employee daily-hours chart. Y-axis labels + a repeating 1px
|
||||
`--empty`-colored gridline background at 25% steps computed from a
|
||||
"nice" rounded max (`Math.ceil` to the next whole hour); bars are solid
|
||||
`--fg`, `--muted` on hover; day-of-month labels render in a *separate*
|
||||
flex row below the fixed-height bar track (not nested inside each bar's
|
||||
percentage-height column) specifically to avoid the bars visually
|
||||
overlapping the labels.
|
||||
- **`.month-nav`** (`frontend/src/components/MonthNav.tsx`) — `[<] label [>]`
|
||||
bordered strip, styled like the design language's tab strip. Deliberately
|
||||
placed *inside* the employee-detail panel next to the chart it controls,
|
||||
not as a page-level control — an earlier version put it at the top of the
|
||||
whole admin stats page, which tested as confusing ("can't browse history
|
||||
on the chart") since it was visually disconnected from what it affected.
|
||||
|
||||
# Related
|
||||
|
||||
- [Zustand stores](./stores.md)
|
||||
6
docs/frontend/index.md
Normal file
6
docs/frontend/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Frontend
|
||||
|
||||
Vite + React + TypeScript PWA, Google Identity Services for sign-in.
|
||||
|
||||
- [Zustand stores](./stores.md) - authStore, attendanceStore, adminStore — where all app logic lives
|
||||
- [TUI monochrome design system](./design-system.md) - the light-mono-tui design language and this app's chart/nav additions
|
||||
62
docs/frontend/stores.md
Normal file
62
docs/frontend/stores.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
type: Frontend Module
|
||||
title: Zustand stores
|
||||
description: authStore, attendanceStore, and adminStore — where all app/fetch logic lives, keeping components thin.
|
||||
resource: frontend/src/store
|
||||
tags: [frontend, zustand, state]
|
||||
timestamp: 2026-08-12T00:00:00Z
|
||||
---
|
||||
|
||||
# Zustand stores
|
||||
|
||||
Convention (matching the user's other `~/mywork` projects): one flat
|
||||
`create<State>()` per concern in `frontend/src/store/<name>Store.ts`,
|
||||
default-exported as `use<Name>Store`. State and actions live together;
|
||||
async/API calls are colocated directly in the actions via `set`/`get` — no
|
||||
separate service layer. No `persist`/`devtools`/`immer`/slices are used
|
||||
here. Components read via selectors (`useXStore(s => s.field)`) and call
|
||||
actions; they don't own fetch/interval logic themselves.
|
||||
|
||||
# `authStore.ts`
|
||||
|
||||
`{ user, loading, loginError }` + `init()` (calls `GET /auth/me` once on
|
||||
app mount, from `App.tsx`), `loginWithGoogle(credential)`, `logout()`. No
|
||||
persistence — the real session lives in the httpOnly cookie, so on reload
|
||||
the store just re-asks the backend via `init()`.
|
||||
|
||||
# `attendanceStore.ts`
|
||||
|
||||
Employee's own clock state. `{ status, stats, live, busy, error }` plus
|
||||
`load()`, `recordEvent(type)`, `startPolling()`/`stopPolling()`.
|
||||
|
||||
- `startPolling` sets two intervals: one that calls `load()` every 60s
|
||||
(keeps the server truth in sync), and one that recomputes `live` every
|
||||
1s from the last-fetched `stats` against a fresh `Date` — via
|
||||
`withLiveTime` (`frontend/src/lib/liveSession.ts`) — so an open shift's
|
||||
duration counts up smoothly instead of freezing until the next poll.
|
||||
`EmployeeApp.tsx` starts/stops this based on `status` (only polls while
|
||||
`working`/`on_break`).
|
||||
- **Gotcha this hit in practice**: `live` is a plain cached field, updated
|
||||
by `set()` inside the tick/poll callbacks — it is deliberately *not* a
|
||||
selector method like `liveStats: () => withLiveTime(...)` called as
|
||||
`useAttendanceStore(s => s.liveStats())`. That pattern returns a new
|
||||
object on every call, which breaks React's `useSyncExternalStore`
|
||||
(used internally by zustand v5) — "Maximum update depth exceeded" from an
|
||||
infinite render loop, since the snapshot is never referentially stable
|
||||
between renders even when nothing actually changed.
|
||||
|
||||
# `adminStore.ts`
|
||||
|
||||
Employee list + org/individual stats + the shared `month` ("YYYY-MM") used
|
||||
by both the summary table and the selected employee's detail chart.
|
||||
`setMonth`/`prevMonth`/`nextMonth` update `month` and re-fetch `loadStats()`
|
||||
plus (if an employee is selected) `selectEmployee()` together, so the
|
||||
summary and the detail chart always show the same period. `removeEmployee`
|
||||
calls the soft-delete endpoint — see [employees](/docs/data-model/employees.md).
|
||||
|
||||
# Related
|
||||
|
||||
- [attendance_events](/docs/data-model/attendance-events.md) — the state
|
||||
machine and `Session` shape these stores fetch
|
||||
- [Attendance API](/docs/api/attendance-routes.md)
|
||||
- [Admin API](/docs/api/admin-routes.md)
|
||||
Reference in New Issue
Block a user