Skip to content
Draft

V1.0 #13

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules
npm-debug.log*
.git
.gitignore
.env
.env.*
!.env.example
backend/data/*.db
backend/data/*.json
frontend/dist
frontend/node_modules
*.md
docs/
LICENSE
.vscode
.idea
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,8 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# Local stuff
tasks.md
tasks.md
todo.md

# Docker Images
*.tar.gz
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# [1.0.0] **Ristretto"

- Production-ready release of CupTrack - digital coffee fund
- PIN-change for user on terminal
- create user on terminal
- fixed: todays coffees not counted
- total users coffees shown on terminal
50 changes: 50 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ─── Stage 1: Build frontend ────────────────────────────────
FROM node:20-alpine AS build

WORKDIR /app

# Install frontend dependencies
COPY frontend/package.json frontend/package-lock.json* frontend/
RUN cd frontend && npm ci

# Copy frontend source and build
COPY frontend/ frontend/
RUN cd frontend && npm run build

# ─── Stage 2: Production ────────────────────────────────────
FROM node:20-alpine

RUN apk add --no-cache curl

WORKDIR /app

# Install backend dependencies only
COPY backend/package.json backend/package-lock.json* backend/
RUN cd backend && npm ci --omit=dev

# Copy backend source
COPY backend/src/ backend/src/

# Copy built frontend from stage 1
COPY --from=build /app/frontend/dist/ frontend/dist/

# Copy version file (needed by health endpoint)
COPY version.json .

# Data directory for SQLite DB (mount as volume)
RUN mkdir -p backend/data

# Run as non-root user
RUN addgroup -S cuptrack && adduser -S cuptrack -G cuptrack
RUN chown -R cuptrack:cuptrack /app
USER cuptrack

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1

CMD ["node", "backend/src/index.js"]
54 changes: 48 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,61 @@
# CupTrack

CupTrack is a digital coffee fund for shared coffee machines, such as those in the office.
It simply tracks which user has drunk how many coffees and deducts the corresponding amounts from their account. CupTrack does not control the coffee machines. The system relies on honesty – just like a haptic coffee fund.
It is web based with a terminal view and an admin's dashboard.

## How it works
Drinking users have account balance and a four digit PIN.
On a machines terminal user select their user, enter their PIN and count a coffee. The predefined amount is deducted from their balance.

Drinking users have an account balance and a four digit PIN.
On a machine's terminal users select their user, enter their PIN and count a coffee. The predefined amount is deducted from their balance.
They also can top up their balance.

> [!TIP]
> This README and the apps documentation is still under construction. Feel free to send me a message, if have questions.
## Tech Stack

- **Frontend:** React 18, TypeScript, Vite, Material-UI
- **Backend:** Express, Node.js (ESM)
- **Database:** SQLite (better-sqlite3, WAL mode)
- **Security:** Helmet, rate-limiting, Zod validation, JWT auth

## Development
The root user on the local dev-server is `root` with password `Coffee`. Change this as well as JWT secret in the .env-file of backend before deploying to production.

```bash
# Backend
cd backend
cp .env.example .env # adjust values
npm install
npm run dev

# Frontend (separate terminal)
cd frontend
npm install
npm run dev
```

The default root user is `root` / `Coffee`. Change credentials in `.env` before deploying.

There is a file `architecture.md` for AI coding agents to understand the architecture of the project without needing to read the entire codebase.

## Deployment

- **Docker:** See [docs/deployment-docker.md](docs/deployment-docker.md)
- **Raspberry Pi:** See [docs/deployment-raspi.md](docs/deployment-raspi.md)

Quick start with Docker:

```bash
cp backend/.env.example backend/.env
# edit backend/.env – set JWT_SECRET and ROOT_PASSWORD
docker compose up -d --build
```

## Migration from lowdb

If upgrading from a version that used `db.json`:

```bash
node backend/src/migrate-from-lowdb.js
```

> [!NOTE]
> This application is vibe coded in most parts. Documenation may be chaos. Structure may doesn't make any sense.
> This application is vibe coded in most parts. Documentation may be chaotic. Structure may not always make sense.
137 changes: 95 additions & 42 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

CupTrack is a digital coffee fund for shared office coffee machines. It tracks which user has consumed how many coffees and deducts the corresponding price from their prepaid balance. CupTrack does **not** control the machines — it relies on user honesty, like a physical coffee fund.

**Version**: 0.2.0 "Cold Coffee" (defined in `/version.json`)
**Version**: 0.3.0 "Cold Coffee" (defined in `/version.json`)

---

Expand All @@ -15,25 +15,43 @@ CupTrack is a digital coffee fund for shared office coffee machines. It tracks w
```
cuptrack/
├── package.json # Root: concurrently runs backend + frontend
├── version.json # App version (major/minor/patch/codeName)
├── version.json # App version (major/minor/patch/codeName/schemaVersion)
├── backend/
│ ├── package.json
│ ├── data/db.json # Runtime JSON database (lowdb, gitignored)
│ ├── data/cuptrack.db # SQLite database (gitignored)
│ └── src/
│ ├── index.js # Express entry point, route mounting
│ ├── index.js # Express entry point, route mounting, server start
│ ├── config.js # Env-based configuration
│ ├── db.js # lowdb setup + root user bootstrap
│ ├── db.js # SQLite setup, schema init, root user bootstrap, migration init
│ ├── dal.js # Data Access Layer (prepared statements for all CRUD)
│ ├── schema.sql # Full database schema (CREATE TABLE IF NOT EXISTS)
│ ├── middleware/
│ │ └── auth.js # JWT verification + admin guard
│ └── routes/
│ ├── auth.js # Login, /me
│ ├── users.js # CRUD users, identifiers, balance
│ ├── machines.js # CRUD coffee machines
│ ├── terminals.js # CRUD terminals (admin)
│ ├── terminalActions.js # Public terminal interactions
│ ├── cashBook.js # Cash book (deposits, withdrawals)
│ ├── stats.js # Dashboard analytics
│ └── settings.js # Global settings (language)
│ │ ├── auth.js # JWT verification + admin guard
│ │ ├── maintenance.js # Maintenance mode guard (blocks non-admin during migrations)
│ │ ├── error-handler.js # Central error handler
│ │ └── request-logger.js # Pino request logging
│ ├── routes/
│ │ ├── auth.js # Login, /me
│ │ ├── users.js # CRUD users, identifiers, balance
│ │ ├── machines.js # CRUD coffee machines
│ │ ├── terminals.js # CRUD terminals (admin)
│ │ ├── terminalActions.js # Public terminal interactions
│ │ ├── cashBook.js # Cash book (deposits, withdrawals)
│ │ ├── stats.js # Dashboard analytics
│ │ ├── settings.js # Global settings (language, log cleanup)
│ │ └── migrations.js # Admin migration API
│ ├── validators/
│ │ └── index.js # Zod validation schemas for all request bodies
│ └── migrations/
│ ├── runner.js # Migration engine (auto + manual migrations)
│ ├── cli.js # CLI tool for migration status/run/backup
│ └── scripts/
│ └── 001-baseline.js # Baseline migration (version 1)
├── docs/
│ ├── api.md
│ ├── migration.md # Migration system documentation
│ ├── deployment-docker.md
│ └── deployment-raspi.md
└── frontend/
├── package.json
├── vite.config.ts
Expand Down Expand Up @@ -69,16 +87,18 @@ cuptrack/

## Tech Stack

| Layer | Technology |
|----------|-----------------------------------------------------|
| Backend | Node.js, Express, ESM |
| Database | lowdb v7 (JSON file at `backend/data/db.json`) |
| Auth | JWT (jsonwebtoken), bcryptjs for password hashing |
| Frontend | React 18, TypeScript, Vite 6 |
| UI | Material-UI (MUI) v5, Emotion |
| Charts | Recharts |
| i18n | i18next + react-i18next (German default, English) |
| Dev | concurrently (runs backend + frontend in parallel) |
| Layer | Technology |
|----------|-----------------------------------------------------------|
| Backend | Node.js, Express, ESM |
| Database | SQLite via better-sqlite3 (WAL mode, foreign keys enabled)|
| Auth | JWT (jsonwebtoken), bcryptjs for password hashing |
| Frontend | React 18, TypeScript, Vite 6 |
| UI | Material-UI (MUI) v5, Emotion |
| Charts | Recharts |
| i18n | i18next + react-i18next (German default, English) |
| Validation | Zod (backend request body validation) |
| Logging | Pino (structured JSON logging) |
| Dev | concurrently (runs backend + frontend in parallel) |

---

Expand Down Expand Up @@ -211,24 +231,47 @@ The running cash balance is computed as `SUM(deposits + balance_topups + anonymo

---

## Database Schema (lowdb)
## Database Schema (SQLite)

The JSON database (`backend/data/db.json`) has these top-level collections:
The database file is `backend/data/cuptrack.db` (SQLite, WAL mode, foreign keys enabled). The full schema is defined in `backend/src/schema.sql` using `CREATE TABLE IF NOT EXISTS`.

```json
{
"users": [],
"machines": [],
"terminals": [],
"logs": [],
"settings": { "language": "de" },
"archivedStats": { "totalCoffees": 0, "coffeesByUser": {}, "coffeesByMachine": {} },
"logCleanups": [],
"cashBook": []
}
```
### Tables

On first start, the backend bootstraps a root admin user from env vars (`ROOT_USERNAME` / `ROOT_PASSWORD`, defaults: `root` / `Coffee`).
| Table | Purpose |
|---------------------|------------------------------------------------------------|
| `users` | Admin, API and drinker accounts (UUID PK) |
| `identifiers` | PIN/RFID/NFC/QR/Kaba credentials linked to users (FK) |
| `machines` | Coffee machines with room + price |
| `terminals` | Web/API terminals linked to a machine (FK), with settings |
| `logs` | Audit log (coffee, balance, login, cashbook, etc.) |
| `cash_book` | Cash flow tracking (deposits, withdrawals, auto entries) |
| `settings` | Global settings (key/value, e.g. `language`) |
| `archived_stats` | Preserved statistics after log cleanup |
| `log_cleanups` | History of admin-triggered log cleanup operations |
| `schema_migrations` | Migration tracking (version, name, type, executedAt) |

### Key Constraints
- Foreign keys: `identifiers.userId → users.id`, `terminals.machineId → machines.id`, etc.
- `users.type` CHECK: `'admin'`, `'api'`, `'drinker'`
- `identifiers.type` CHECK: `'pin'`, `'rfid'`, `'nfc'`, `'qr'`, `'kaba_nfc'`
- Terminal settings (`quickButtons*`, `alphabetFilter`) stored as columns with defaults

On first start, the backend executes `schema.sql`, then bootstraps a root admin user from env vars (`ROOT_USERNAME` / `ROOT_PASSWORD`, defaults: `root` / `Coffee`).

---

## Database Migrations

CupTrack uses a custom migration system (no external package). See [`docs/migration.md`](docs/migration.md) for full details.

### Key Concepts
- **Migration scripts** live in `backend/src/migrations/scripts/NNN-name.js` (linear integer versioning)
- **Auto migrations**: Backward-compatible changes, run automatically on server start
- **Manual migrations**: Breaking changes requiring admin confirmation via the dashboard
- **Maintenance mode**: When manual migrations are pending, only auth and migration API routes are accessible
- **Backups**: Created automatically before each migration via `better-sqlite3`'s native `backup()` API (max 5 kept)
- **Schema version**: Tracked in `version.json` (`schemaVersion` field) and in the `schema_migrations` table
- **CLI**: `node backend/src/migrations/cli.js status|run|backup` for headless deployments (e.g. Raspberry Pi)

---

Expand Down Expand Up @@ -309,6 +352,12 @@ All routes are mounted under `/api`.
|--------|------|------|------------------------------|
| GET | `/` | None | Returns version info |

### Migrations — `/api/admin/migrations` (JWT + Admin)
| Method | Path | Auth | Purpose |
|--------|--------|------------|------------------------------------------------|
| GET | `/` | JWT+Admin | Get migration status (current version, pending)|
| POST | `/run` | JWT+Root | Execute a single manual migration |

---

## Authentication Flows
Expand Down Expand Up @@ -351,7 +400,7 @@ All routes are mounted under `/api`.
## Key Patterns & Conventions

- **ESM throughout** — Backend uses ES modules (`import/export`), configured via `"type": "module"` in backend/package.json.
- **No ORM** — Data access is direct via `db.data.users`, `db.data.machines`, etc. (lowdb). Mutations are followed by `await db.write()`.
- **Data Access Layer** — All database access goes through `backend/src/dal.js` using prepared statements (better-sqlite3). No raw SQL in route handlers.
- **UUID for IDs** — All entities use `uuid` v4 for primary keys.
- **Slug-based terminal access** — Terminals are accessed publicly by auto-generated slug (from name), not by ID.
- **Audit logging** — Coffee consumption, balance changes, and logins are logged to the `logs` collection.
Expand All @@ -361,7 +410,8 @@ All routes are mounted under `/api`.
- **Root user protection** — The bootstrapped root admin cannot be edited or deleted via the API.
- **Cash book** — Tracks physical cash in the office coffee fund. Manual deposits/withdrawals are created by admins. Automatic entries are created when users top up their balance at a terminal (`balance_topup`) or when a guest uses the anonymous coffee button (`anonymous_coffee`). Admin balance edits via the dashboard do **not** create cash book entries (they are considered error corrections). Only manual entries can be deleted.
- **Guest coffee** — Terminals offer a "Guest Coffee" button on the home screen for anonymous, account-less coffee purchases. This creates a cash book deposit entry with the machine's coffee price.
- **Log cleanup** — Admins can manually delete logs older than one year via the Settings page. Before deletion, coffee statistics are aggregated into `archivedStats` so dashboard totals (total coffees, top drinkers, popular machines) remain accurate. Each cleanup is recorded in `logCleanups` with timestamp, admin name, count, and affected time range.
- **Log cleanup** — Admins can manually delete logs older than one year via the Settings page. Before deletion, coffee statistics are aggregated into `archived_stats` so dashboard totals (total coffees, top drinkers, popular machines) remain accurate. Each cleanup is recorded in `log_cleanups` with timestamp, admin name, count, and affected time range.
- **Maintenance mode** — When manual database migrations are pending, the `maintenance` middleware blocks all non-essential routes (only auth, migrations, and health remain accessible). The admin sees a banner in the dashboard with migration details and a confirmation dialog.

---

Expand All @@ -373,3 +423,6 @@ All routes are mounted under `/api`.
| `JWT_SECRET` | `cuptrack-dev-secret-change-in-production` | JWT signing key |
| `ROOT_USERNAME` | `root` | Initial admin user |
| `ROOT_PASSWORD` | `Coffee` | Initial admin pass |
| `NODE_ENV` | (unset) | `production` in Docker |
| `CORS_ORIGIN` | (unset) | Allowed CORS origin |
| `LOG_LEVEL` | `info` | Pino log level |
18 changes: 15 additions & 3 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# Server
PORT=3000
JWT_SECRET=change-this-in-production
ROOT_USERNAME=root
ROOT_PASSWORD=Coffee
NODE_ENV=production
LOG_LEVEL=info

# Security – MUST be changed in production (≥32 characters)
JWT_SECRET=change-me-to-a-long-random-secret-at-least-32-chars

# Root admin account
# Will be created on first start, if no root user exists.
# You can't change it here afterwards.
ROOT_USERNAME=admin
ROOT_PASSWORD=change-me-in-production

# CORS – comma-separated origins, or * for development only
CORS_ORIGIN=http://localhost:3000
Loading