Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .env.prod.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Pin deployments to an immutable OpenClockwork release. Do not use `latest`
# for production upgrades.
OPENCLOCKWORK_VERSION=1.2.2
OPENCLOCKWORK_VERSION=1.3.0
# Keep these names unchanged across upgrades.
OPENCLOCKWORK_DB_VOLUME=openclockwork-db-data-prod
OPENCLOCKWORK_ATTACHMENTS_VOLUME=openclockwork-attachments-prod
Expand Down
9 changes: 7 additions & 2 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Employee submits
| Absence administration | Record and review sickness, training, and flextime entries |
| Approval operations | Manager/HR inboxes, bulk actions, correction loops, and workflow history |
| Terminal administration | Configure, activate, pair, monitor, revoke, re-pair, deactivate, or permanently delete tablet kiosks |
| Working-time reports | HR-only start, end, break, gross, net, approval, and CSV reporting independent of projects |
| Working-time reports | HR-only start, end, break, gross, net, approval, and CSV reporting, with optional clocking locations |

Production starts with an empty database. The interactive
`prisma/create-admin.ts` bootstrap creates exactly one first HR administrator,
Expand All @@ -166,9 +166,14 @@ bookings, and customer-facing activity reports in one administrative workflow.
- Detailed evaluations filtered by period, employee, project, and order
- Customer-facing activity report and CSV export
- Project-independent HR working-time report for all closed, non-rejected entries
- Opt-in clock-in and clock-out locations in the report table and CSV export,
combining durable terminal labels with available GPS coordinates and accuracy

Exports contain employee names and working-time data and must be handled as
personal data under the organisation's access and retention policies.
personal data under the organisation's access and retention policies. Exact
clocking locations are excluded by default and require an explicit HR action;
operators must document a lawful purpose and suitable retention period before
using them.

## Compliance-oriented domain logic

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ installable PWA on phones, tablets, and desktops.
bulk actions, and workflow history.
- **Projects and reporting.** Assign employees, structure projects by service
order, compare PLAN and IST hours, edit booking targets, split entries, and
export customer or working-time reports.
export customer or working-time reports with optional clock-in/out locations.
- **Self-hosted and API-first.** PostgreSQL, NestJS, React, OpenAPI, Socket.IO,
Docker, and an Azure reference deployment—without SaaS lock-in.
- **German and English.** Centralised translations, locale-aware dates, and a
Expand All @@ -85,8 +85,9 @@ The security model is deliberately separate from an employee session:
- daily signing material is derived from a dedicated `TERMINAL_QR_SECRET`, not
from `JWT_SECRET`;
- clock-in/out uses the authenticated employee identity from the bearer token;
- geofence, position, accuracy, and radius snapshots remain in the historical
booking audit record even if the terminal is later deleted permanently;
- terminal location labels, geofence positions, accuracy, and radius snapshots
remain in the historical booking audit record even if the terminal is later
deleted permanently;
- pairing, kiosk, and mobile camera/location flows are documented for trusted
local HTTPS and managed iPad deployments.

Expand All @@ -106,6 +107,11 @@ collective agreements, payroll integrations, privacy requirements, backups,
monitoring, and incident procedures. OpenClockwork provides technical controls;
it is not legal advice.

Exact clock-in and clock-out locations are excluded from HR working-time reports
by default. HR administrators must explicitly include them in the on-screen
report and CSV export; operators remain responsible for a lawful purpose,
appropriate access, and retention periods for this personal data.

See the [latest release](https://github.com/patrickschiller/openclockwork/releases/latest)
and read [UPGRADING.md](UPGRADING.md) before changing an existing installation.

Expand Down
56 changes: 31 additions & 25 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,51 @@
# OpenClockwork v1.2.2
# OpenClockwork v1.3.0

## Highlights

- Prevents mixed web/API rollouts from exposing terminal controls against an
older API. This fixes the production failure that surfaced as
`Cannot POST /api/terminals` even though the terminal UI was already visible.
- Reports the running API release version through `/api/health`. The web
container waits for the matching API version before nginx accepts browser
traffic, so an incomplete rollout fails safely instead of serving an
incompatible application.
- Makes production Docker Compose wait for API health before starting the web
service and documents the required API-before-web order for separate Azure
Container App revisions.
- Adds regression coverage for creating and activating the very first terminal
after production administrator bootstrap, with no seeded terminal or support
prompt state.
- Adds an HR opt-in for clock-in and clock-out locations in the
project-independent working-time report and its CSV export. Location data is
excluded from report responses by default.
- Combines available GPS coordinates and accuracy with durable terminal
location labels. Terminal labels are captured at booking time so historical
reports remain stable after a terminal is renamed or deleted.
- Secures employee-directory and request-workflow APIs with bearer
authentication. Request actors and employees are derived from the
authenticated session instead of client-supplied identity fields.
- Extends regression coverage for location reporting, terminal deletion,
authenticated workflow transitions, database upgrades, and generated API
contracts.

## Upgrade notes

- Back up PostgreSQL and request attachments before upgrading.
- Set `OPENCLOCKWORK_VERSION=1.2.2` and follow the
[upgrade guide](https://github.com/patrickschiller/openclockwork/blob/v1.2.2/UPGRADING.md).
- Set `OPENCLOCKWORK_VERSION=1.3.0` and follow the
[upgrade guide](https://github.com/patrickschiller/openclockwork/blob/v1.3.0/UPGRADING.md).
- Existing production volumes are retained; do not use `docker compose down -v`.
- This patch contains no database migration and does not add demo data to
production installations.
- When API and web are deployed as separate services, deploy the `1.2.2` API
first and wait for `/api/health` to report version `1.2.2`; then deploy the
web image. Docker Compose performs this ordering automatically.
- Update external integrations that call employee or request endpoints: they
must send a valid JWT bearer token and must not rely on request-body
`employeeId` or `actorId` values to select the acting user.
- When API and web are deployed separately, deploy the `1.3.0` API first and
wait for `/api/health` to report version `1.3.0`; then deploy the web image.
Docker Compose performs this ordering automatically.

## Database migrations

None.
- `20260901120000_working_time_report_locations` adds nullable terminal location
label snapshots for clock-in and clock-out and backfills existing
terminal-linked entries where possible.
- The migration is additive and contains no destructive schema change.

## Breaking changes

None.
- All `/api/employees` and `/api/requests` endpoints now require bearer
authentication. Workflow identity is taken from the authenticated token;
unauthenticated calls and attempts to select another actor through request
bodies are rejected or ignored as appropriate.

## Docker images

- `ghcr.io/patrickschiller/openclockwork-api:1.2.2`
- `ghcr.io/patrickschiller/openclockwork-web:1.2.2`
- `ghcr.io/patrickschiller/openclockwork-api:1.3.0`
- `ghcr.io/patrickschiller/openclockwork-web:1.3.0`

## Known issues

Expand Down
12 changes: 6 additions & 6 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Only publish a release from a green `main` commit. Stable releases follow
semantic versioning and use matching metadata in five places:

- `package.json`: `MAJOR.MINOR.PATCH`
- `apps/api/src/main.ts` and `apps/api/src/generate-openapi.ts`:
`MAJOR.MINOR.PATCH`
- `apps/api/src/main.ts`, `apps/api/src/generate-openapi.ts`, and the health DTO
example: `MAJOR.MINOR.PATCH`
- `RELEASE_NOTES.md`: `# OpenClockwork vMAJOR.MINOR.PATCH`
- `.env.prod.example`: `OPENCLOCKWORK_VERSION=MAJOR.MINOR.PATCH`
- Git tag: `vMAJOR.MINOR.PATCH`
Expand All @@ -30,8 +30,8 @@ annotated tag on the exact merge commit and push it:
```bash
git switch main
git pull --ff-only
git tag -a v1.2.2 -m "OpenClockwork v1.2.2"
git push origin v1.2.2
git tag -a v1.3.0 -m "OpenClockwork v1.3.0"
git push origin v1.3.0
```

The release workflow verifies the tag and notes, runs Nx and API end-to-end
Expand All @@ -46,8 +46,8 @@ pulled anonymously by self-hosted installations.
## Verify the published release

```bash
docker pull ghcr.io/patrickschiller/openclockwork-api:1.2.2
docker pull ghcr.io/patrickschiller/openclockwork-web:1.2.2
docker pull ghcr.io/patrickschiller/openclockwork-api:1.3.0
docker pull ghcr.io/patrickschiller/openclockwork-web:1.3.0
```

Confirm that the GitHub Release is marked latest, contains the curated upgrade
Expand Down
18 changes: 16 additions & 2 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,31 @@ secrets. Azure deployments must likewise set the secure
`terminalQrSecret` Bicep parameter (for example in an untracked private
parameter file); the deployment stores it as a separate Key Vault secret.

This release also hardens the existing live-booking API. `GET /api/timeentries`,
Existing releases from 1.2.0 onward also harden the live-booking API. `GET /api/timeentries`,
`POST /api/timeentries/clock-in`, and `POST /api/timeentries/clock-out` now
require an employee bearer token. Clock-in/out always use the employee identity
from that token; a legacy `employeeId` in the request body is ignored. Update
external clients before rollout and regenerate them from
`apps/api/openapi.json` where applicable.

## Required for 1.3.0

All employee-directory and request-workflow endpoints now require a JWT bearer
token. Request creation, approval, rejection, substitution, cancellation, and
bulk operations derive the acting employee from that token instead of trusting
`employeeId` or `actorId` in a request body. Update external clients to
authenticate these calls and use the regenerated OpenAPI contract before the
rollout.

Release 1.3.0 adds nullable clock-in and clock-out terminal-location snapshots
to `TimeEntry`. The forward migration backfills labels for entries that still
reference a terminal. No manual SQL is required; normal API startup applies the
migration before serving traffic.

## 2. Select and pull the release

Set `OPENCLOCKWORK_VERSION` in `.env.prod` to the exact version from the GitHub
Release, for example `1.2.2`. Do not use `latest` for a controlled production
Release, for example `1.3.0`. Do not use `latest` for a controlled production
upgrade.

```bash
Expand Down
15 changes: 15 additions & 0 deletions apps/api-e2e/src/api/terminals.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,15 @@ describe('Tablet terminals', () => {
TERMINAL_INPUT.maxAccuracyMeters,
);
expect(firstOut.body.entry.clockOutPositionTimestamp).toBeTruthy();
const storedEntry = await ctx.prisma.timeEntry.findUniqueOrThrow({
where: { id: firstOut.body.entry.id },
});
expect(storedEntry.terminalLocationLabel).toBe(
TERMINAL_INPUT.locationLabel,
);
expect(storedEntry.clockOutTerminalLocationLabel).toBe(
TERMINAL_INPUT.locationLabel,
);

await ctx.http
.post('/api/terminals/scan')
Expand Down Expand Up @@ -547,6 +556,12 @@ describe('Tablet terminals', () => {
expect(historicalEntry.clockOutTerminalId).toBeNull();
expect(historicalEntry.clockInChallengeId).toBeNull();
expect(historicalEntry.clockOutChallengeId).toBeNull();
expect(historicalEntry.terminalLocationLabel).toBe(
TERMINAL_INPUT.locationLabel,
);
expect(historicalEntry.clockOutTerminalLocationLabel).toBe(
TERMINAL_INPUT.locationLabel,
);
expect(Number(historicalEntry.latitude)).toBeCloseTo(
TERMINAL_INPUT.latitude,
);
Expand Down
38 changes: 38 additions & 0 deletions apps/api-e2e/src/api/working-time-reports.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ describe('Project-independent working-time reports', () => {
clockIn: new Date('2026-08-10T07:00:00.000Z'),
clockOut: new Date('2026-08-10T15:00:00.000Z'),
status: 'Approved',
terminalLocationLabel: 'Büro Würzburg',
latitude: 49.791304,
longitude: 9.953355,
accuracyMeters: 12,
clockOutLatitude: 49.8,
clockOutLongitude: 9.94,
clockOutAccuracyMeters: 18.4,
},
{
employeeId: manager.id,
Expand Down Expand Up @@ -105,12 +112,37 @@ describe('Project-independent working-time reports', () => {
.set('Authorization', `Bearer ${hrToken}`)
.expect(200);
expect(hrReport.body.rows).toHaveLength(3);
expect(hrReport.body.rows[0]).not.toHaveProperty('clockInLocation');
expect(hrReport.body.rows[0]).not.toHaveProperty('clockOutLocation');
expect(hrReport.body.totals).toEqual({
grossMinutes: 660,
breakMinutes: 30,
netMinutes: 630,
});

const reportWithLocations = await ctx.http
.get(
'/api/reports/working-times?from=2026-08-01&to=2026-08-31&includeLocations=true',
)
.set('Authorization', `Bearer ${hrToken}`)
.expect(200);
expect(reportWithLocations.body.rows[0]).toMatchObject({
clockInLocation: {
label: 'Büro Würzburg',
latitude: 49.791304,
longitude: 9.953355,
accuracyMeters: 12,
},
clockOutLocation: {
label: null,
latitude: 49.8,
longitude: 9.94,
accuracyMeters: 18.4,
},
});
expect(reportWithLocations.body.rows[1].clockInLocation).toBeNull();
expect(reportWithLocations.body.rows[1].clockOutLocation).toBeNull();

const onlyDirect = await ctx.http
.get(
`/api/reports/working-times?from=2026-08-01&to=2026-08-31&employeeId=${report.id}`,
Expand Down Expand Up @@ -146,6 +178,12 @@ describe('Project-independent working-time reports', () => {
.get('/api/reports/working-times?from=2025-01-01&to=2026-12-31')
.set('Authorization', `Bearer ${hrToken}`)
.expect(400);
await ctx.http
.get(
'/api/reports/working-times?from=2026-08-01&to=2026-08-31&includeLocations=yes',
)
.set('Authorization', `Bearer ${hrToken}`)
.expect(400);
await ctx.http
.get('/api/reports/working-times?from=2026-08-01&to=2026-08-31')
.set('Authorization', `Bearer ${employeeToken}`)
Expand Down
58 changes: 56 additions & 2 deletions apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,15 @@
"format": "uuid",
"type": "string"
}
},
{
"name": "includeLocations",
"required": false,
"in": "query",
"schema": {
"default": false,
"type": "boolean"
}
}
],
"responses": {
Expand Down Expand Up @@ -2660,7 +2669,7 @@
"info": {
"title": "OpenClockwork API",
"description": "Self-hostable working-time tracker — REST + WebSocket surface.",
"version": "1.2.2",
"version": "1.3.0",
"contact": {}
},
"tags": [],
Expand Down Expand Up @@ -2730,7 +2739,7 @@
},
"version": {
"type": "string",
"example": "1.2.2"
"example": "1.3.0"
},
"utcTimestamp": {
"type": "string",
Expand Down Expand Up @@ -3121,6 +3130,33 @@
},
"required": ["id", "firstName", "lastName"]
},
"WorkingTimeReportLocationDto": {
"type": "object",
"properties": {
"label": {
"type": "string",
"nullable": true
},
"latitude": {
"type": "number",
"nullable": true,
"minimum": -90,
"maximum": 90
},
"longitude": {
"type": "number",
"nullable": true,
"minimum": -180,
"maximum": 180
},
"accuracyMeters": {
"type": "number",
"nullable": true,
"minimum": 0
}
},
"required": ["label", "latitude", "longitude", "accuracyMeters"]
},
"WorkingTimeReportRowDto": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -3162,6 +3198,24 @@
"netMinutes": {
"type": "number",
"minimum": 0
},
"clockInLocation": {
"nullable": true,
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/WorkingTimeReportLocationDto"
}
]
},
"clockOutLocation": {
"nullable": true,
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/WorkingTimeReportLocationDto"
}
]
}
},
"required": [
Expand Down
Loading
Loading