Clockora Manual
Clockora — User Manual
Time tracking: hours worked, by employee and by project. Version 4.1.3 [src: app/src/App.php:8].
About this manual
Every statement here was written by reading Clockora's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.
Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.
What Clockora is
It tracks hours worked only [src: app/src/Timesheet.php:17].
It is neither invoicing nor payroll: it never stores a billing rate, never computes a money amount, never touches cards, bank rails or crypto, and initiates no payments [src: app/src/Timesheet.php:18].
Projects are labels for grouping hours, nothing more [src: app/src/Timesheet.php:19]. If you are looking for a rate against a project so the product can produce an invoice, it is not here and is not meant to be.
Everything is whole minutes
Worked time is integer minutes end to end, and the source spells out why that matters: a clock span is a difference of two minutes-of-day integers, a manual entry parses straight to integer minutes, and every sum is done in PHP with a final clamp — never a SQL SUM(), which can overflow and fail under strict mode [src: app/src/Timesheet.php:11].
Because minutes are integers, period totals reconcile exactly [src: app/src/Timesheet.php:14].
⭐ A day cannot hold more than a day
This is the guard worth knowing about, and the source records the bug that produced it.
A single entry is capped at a 24-hour span [src: app/src/Timesheet.php:30]. That alone was not enough:
Ten stacked manual 8:00 entries produced an accepted 80-hour day — each row legal, the sum physically impossible. [src: app/src/Timesheet.php:35]
So there is a second ceiling: one employee's entries dated on one day may not total more than 1440 minutes, however each entry was recorded [src: app/src/Timesheet.php:33], and the check runs before an entry is accepted [src: app/src/Timesheet.php:43].
The suite guards it at the source level for every recording method, not just the one the bug arrived through.
One clarification on cross-midnight entries
A clock entry that crosses midnight is dated on its start day, so "the day's total" strictly means the total of entries dated that day — which the source notes is exactly what payroll sums [src: app/src/Timesheet.php:37].
Worth knowing if you compare a day's figure against a wall clock: a night shift starting on Monday counts against Monday.
Overlaps
Entries are placed on one absolute minute timeline [src: app/src/Timesheet.php:149] so overlap detection works across midnight, and a clash check can exclude the entry being edited [src: app/src/Timesheet.php:177].
Overtime is a flag, never a payment
The section is headed exactly that in the source — overtime FLAG (never pay) [src: app/src/Timesheet.php:255].
The threshold is per employee where they have their own target, otherwise the business default [src: app/src/Timesheet.php:257]. A threshold of zero or less disables the flag entirely [src: app/src/Timesheet.php:264], and minutes above the threshold are informational only [src: app/src/Timesheet.php:270].
Overtime is counted by the week, not by the range
A multi-week report counts how many whole weeks exceeded the threshold [src: app/src/Timesheet.php:281], rather than scaling one number across the range.
The source gives the reasoning in a half-sentence that is exactly right: overtime is a weekly concept [src: app/src/Timesheet.php:279]. Forty-five hours in one week and thirty-five in the next is one overtime week, not ten hours spread over two — and a product that averaged them would report neither honestly.
Status
Four statuses: draft, submitted, approved, rejected [src: app/src/Timesheet.php:48].
Users and roles
Clockora's viewer role is granted entry.read and nothing more [src: app/controllers/v3.php:22] — the narrowest viewer grant in this family of products.
My time — a login for each employee (3.2)
A fourth role, employee, belongs to a login bound to ONE employee record [src: app/controllers/v3.php, _ck_perms() and _ck_my_employee()]. It holds only self.read and self.write. One rule, run before any route, keeps it on My time [src: app/controllers/staff.php, ck_guard_employee_scope()]: a page elsewhere sends the person back to their own, and a write elsewhere is refused. On My time a person clocks in and out, adds their own hours by hand, removes their own draft or rejected entries, and submits their week. The employee is always taken from the login, never from the form. Approving stays with an administrator.
An administrator makes the login from the employee's page (Give them a login, a single-use invite valid 7 days, for the email on the record) and can switch it off there.
The kiosk (3.2)
A kiosk link (Settings → Kiosk) opens a PIN pad on a shared tablet [src: app/controllers/staff.php, kiosk_page() and kiosk_punch()]. The link carries a long random token, stored only as a hash and shown once; switching it off stops the tablet at once. Each person's PIN is 6 digits, unique among active staff, and stored only as a keyed hash (HMAC) under a per-install key [src: app/controllers/staff.php, _ck_pin_issue()]. Ten wrong PINs in ten minutes pause that tablet. The kiosk takes no session and records no location.
One clock for every door
The grid, My time and the kiosk start and stop timers through _ck_clock_in() and _ck_clock_out() [src: app/controllers/v3.php]. A stop goes through the ordinary entry validator, so it is refused for an overlap, a day over 24 hours or a locked period exactly as a typed entry is — and the timer keeps running, so the time is not lost.
Breaks, payroll layouts and Excel (3.2)
Unpaid breaks. A clock entry can carry a break: the worked minutes are the span less the break, and a break as long as the shift is refused [src: app/controllers/app.php, _entry_from_input()]. On a running clock, Break starts and ends one (the timesheet, My time and the kiosk all call _ck_break_toggle()); clocking out takes every break off, including one still running [src: app/controllers/v3.php, _ck_clock_out()]. A manual duration is already net, so it takes no break.
Payroll layouts. Pay periods → Payroll: one row per person (the 3.1 layout, unchanged), one row per person per day, or one row per person with decimal hours. Decimal hours are rounded half up to 2 places by integer arithmetic from the exact minutes, which every layout keeps beside them [src: app/controllers/v3.php, _ck_payroll_rows(), _ck_hours_2dp()]. Approved entries only, as before.
Excel. Entries and every payroll layout also download as .xlsx [src: app/src/Xlsx.php]: numbers stay numbers, and text is stored as text, so nothing in a cell can run as a formula.
Backups and Settings are for administrators
Both backup downloads — the JSON export and the SQLite file — and the Settings page ask for the administrator role [src: app/controllers/api.php, backup_json() and backup_sqlite(); app/controllers/app.php, settings_page()]. Viewer and member accounts are refused, and the menu and the Security page show those links to administrators only. Before 3.1.4 a sign-in alone was enough to download the whole database, and any account could read the mail server settings.
One server name
The REST interface, the agent specification and its unauthenticated probe all name the server clockora [src: app/controllers/v3.php]. Before 3.1.4 the probe and the specification said Clockora.
The API and agent access
Five tools are exposed to an agent [src: app/controllers/v3.php:341]: listing timesheets, timesheet detail and period totals on the read side; submitting a timesheet and deciding one on the write side.
Note that submitting and deciding are separate tools, mirroring the separation of authorities the roles draw.
What Clockora does not do
It does not invoice and it does not pay [src: app/src/Timesheet.php:18] — no rates, no money amounts, no payments.
It does not hold a rate against a project [src: app/src/Timesheet.php:19]. Projects group hours.
It does not accept a physically impossible day [src: app/src/Timesheet.php:33].
It does not turn an overtime flag into money [src: app/src/Timesheet.php:255], and it does not average overtime across weeks [src: app/src/Timesheet.php:279].
It does not sum time in SQL [src: app/src/Timesheet.php:11].
Looking after your install (from 4.0)
Updates from inside Clockora
Updates in the menu opens the Updates page (/updates) [src: app/views/updates.php]. Paste your license key once and press Save key: either key works, the one in your Lemon Squeezy receipt or the one on your order page (it starts with OWX-). Check for updates asks the store for the newest release your license covers; Clockora never checks by itself.
When a new version is ready, Back up and update to that version backs up your data and the current version, installs the new one and checks it. If the update is interrupted, or the new version's database step or that check fails, the previous version is put back by itself. Clockora is unavailable for about a minute while this runs. The update replaces Clockora's files, including any you edited, except .htaccess, .user.ini and robots.txt: an edited copy of those is kept, with the new version's copy beside it as .new to compare and merge. The Health page lists any file that differs from the version installed, so you can see that before you update. A copy of the previous version (data/updates/code-<version>/) and a backup of your data from just before the update stay in data/updates/ [src: app/src/Updater.php]. If Clockora shows an error page after an update anyway, that is your way back: copy the files from code-<version>/ over the app's files, then restore the backup on the backup page. Updating from the page needs PHP's sodium and Phar extensions; if one is missing, the Updates and Health pages say so and updates stay by hand. Where Clockora is hosted for you, the host updates it and the page says so.
Reaching 4.0 from a 3.x version is the last update done by hand: replace the files as before (config.php and data/ stay, and the database migrates itself on the next page load). From 4.0 on, the Updates page does it.
Starting with sample data
The installer offers Start with sample data: a studio team with projects and two weeks of time entries to try things on. You can remove it in one click. [src: app/install/index.php:125]. It is ticked by default; untick it to start empty. The sample is added when you open the first page after the installer.
When you are done with it, press the remove button in the sample banner. It asks "Remove the sample people, projects and time entries? Your own records stay." Only the sample goes: your own records stay, and a sample record that one of your own records uses is kept and named in the report [src: app/src/SampleData.php].
Backups by themselves, and a copy somewhere else
On the Restore page (/restore), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:101]. The first visit at least 24 hours after the last backup starts the next one, after the page has loaded, so no cron job is needed. The newest backups stay in data/backups/ (14 unless you change Backups to keep), with credentials left out of them (a restore keeps the live values for those). Back up now takes one straight away.
To keep a copy somewhere else as well, fill in an S3-compatible bucket (Backblaze B2, Wasabi, AWS S3 or your own MinIO): the bucket address (an https endpoint), the region, the bucket name, an optional folder, and the access key ID and secret key. Leave the address empty to keep backups on this server only. The copy goes only to the bucket you set; nothing is sent to Ownware. Once a week Clockora reads the newest backup back through the restore's own dry run and keeps the result for the Health page [src: app/src/BackupTargets.php].
On Nginx: keep data/ private
On Apache, the .htaccess that ships with Clockora keeps data/ (your backups included), src/, config.php and the other private files off the web. Nginx does not read .htaccess, so add these three blocks to your server block, above the block that passes .php files to PHP (Nginx uses the first one that matches):
location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }
Then open /config.php in a browser: it should answer 403. Only the template files under views/ are closed, not the folder: the app's own saved-view addresses start with /views/ too, and they must reach it.
A complete server block for a new site (PHP-FPM 8.3; change the name, the folder and the socket to yours):
server {
listen 80;
server_name example.com;
root /var/www/clockora/app;
index index.php;
location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }
location = /install/ { rewrite ^ /install/index.php last; }
location / { try_files $uri /index.php?$query_string; }
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
The Health page
Health in the menu (/health) shows, one line each, whether the install is looking after itself: the PHP version and extensions, disk space, whether data/ is writable, email, the age of the last backup, the offsite copy, the weekly check, whether an update is waiting, and any file that differs from the version installed. Each line says what to do when it is not fine. The page only reads: it never changes anything and never contacts anything [src: app/src/Health.php].
Connect it to other tools (Zapier, Make, n8n)
Clockora sends a webhook the moment something happens and accepts calls to its API, so Zapier, Make and n8n can connect to it without a plugin. The steps, the events and example recipes are in API.md, in the section "Connect it to Zapier, Make or n8n"; the API page of Clockora on the store shows the same file [src: app/API.md].
New in 4.1.2: sign-in limits
New in 4.1.2 (security update): Two-step codes are now limited (10 wrong per person or per address in 10 minutes), and wrong passwords count per account as well as per address. An installed app whose database does not answer keeps its web installer closed instead of showing the install form. On Ownware Cloud, opening the app from the console starts a fresh session.
Wrong passwords are counted per address and, since 4.1.2, per account too (the email typed, in any case, stored only as a hash). After 10 wrong passwords in 10 minutes the sign-in at /login answers "Too many attempts — wait a few minutes and try again." for that address, and for that account from any address. A right password clears both counts [src: app/src/LoginRate.php:40].
Wrong two-step codes (for people who turned two-step sign-in on under /security) are counted per person and per address. After 10 wrong codes in 10 minutes the code step answers "Too many wrong codes. Wait ten minutes, then sign in again. A recovery code works in place of the code." even for the right code, and the half-finished sign-in is dropped, so the password is asked again [src: app/src/LoginRate.php:91]. The right code clears that person's count; an address's count runs out ten minutes after its wrong codes. Before 4.1.2 the code step had no limit.
If you are locked out: wait ten minutes, then sign in again with your password and a current code, or one of your recovery codes (each works once). If you lost both the authenticator and the recovery codes, whoever runs the server can turn two-step sign-in off for you in Clockora's database (by default the SQLite file in data/, or your MySQL database): UPDATE users SET totp_secret = '' WHERE email = 'you@example.com'; Then sign in with your password and turn it on again under /security.
On Ownware Cloud, opening Clockora from the console starts a fresh session [src: app/index.php:64].
The web installer stays closed when the database does not answer: with config.php in place, /install now answers 503 "This app is already installed (config.php exists), but its database is not answering, so the installer stays closed." and writes nothing until the database answers again. Before 4.1.2 a database outage (or a missing SQLite file) showed the install form there, and whoever found it could set up a new administrator. To install again from scratch, remove config.php first [src: app/install/index.php:21].
New in 4.1.3: security update
New in 4.1.3 (security update): no other site can put Clockora's pages in a frame; the /mcp address refuses requests from other sites' pages; company sign-in is stricter; and the audit CSV treats one more first character as text. Nothing else changed: no feature, screen, setting or database table, and your data, config.php and data/ are not touched.
No other site can frame these pages. Every page now answers Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so a page on another site cannot show Clockora in a hidden frame and trick a signed-in person into pressing a button [src: app/index.php:4]. The installer answers frame-ancestors 'none' and X-Frame-Options: DENY [src: app/install/index.php:4]. If you had Clockora framed inside another site of your own, that frame now stays empty; link to it instead.
/mcp refuses other sites. A request whose Origin header names another site gets 403 before anything is read. A desktop or server client sends no Origin and is served as before, and so is a page at Clockora's own address [src: app/src/Mcp.php:272].
Single sign-on. The sign-in now uses PKCE (S256), so an intercepted sign-in code is of no use on its own; a sign-in that comes back to a different browser session is asked to start again [src: app/src/Oidc.php:228]. With Google as the provider, a company address needs a Google Workspace account of that same domain (the token's hd claim), and Google must report the address as verified, so a personal Google account made with a work address no longer gets in [src: app/src/Oidc.php:215]. Gmail addresses work as before, other providers are unchanged, and password sign-in is not affected.
The audit CSV. A cell that starts with a carriage return now opens in a spreadsheet as text, as cells starting with = + - @ or TAB already did [src: app/src/AuditLog.php:138].
Docker. The Dockerfile now installs PHP's zip extension, so a Docker install can make the backup with files [src: app/Dockerfile:7].
Also changed, with no effect on a running install: the corner card on a public demo offers only the purchase, and API.md no longer shows the config example of a product we retired.