/projects/hr-time-tracking
2021–2024
Remote-team HR with screen recording & time tracking
- client:
- Spondon IT — internal platform for a fully remote company
- role:
- Developer → TPM
- Laravel
- Vue.js
- MySQL
- AWS S3
01 — the problem
What was actually at stake
A fully remote team needs what an office gives for free: visibility into whether work is actually happening — without a daily interrogation. Spondon ran its whole delivery operation remote, so the internal HR system had to track tasks and time with screen-recording evidence, while staying fair to the people being recorded and cheap on their laptops and bandwidth.
02 — the architecture
How it was built
Capture clients batch screenshots and activity samples locally, then upload to S3 via short-lived signed URLs — the application server never proxies media bytes. Time entries reconcile client clocks against server windows so offline stretches and clock drift don't corrupt timesheets.
Sessions link to tasks, so a manager reads a timeline of work-against-tickets rather than raw surveillance footage; retention policies expire raw artifacts while billing-grade summaries persist. The same HR core later carried the company's standard modules — attendance, leave, payroll inputs.
- upload
- pre-signed S3, zero server proxying
- time
- drift-tolerant reconciliation windows
- context
- sessions linked to tasks, not raw footage
- retention
- artifacts expire, summaries persist
03 — the visuals
What it looks like
screenshot pending (NDA-safe crop)
drop /public/images/projects/hr-time-tracking/01.jpg · 1600×900 · see docs/images.md
screenshot pending (NDA-safe crop)
drop /public/images/projects/hr-time-tracking/02.jpg · 1600×900 · see docs/images.md
04 — from the codebase
Issuing short-lived signed upload slots
public function reserve(UploadBatchRequest $request): JsonResponse
{
$session = $request->session();
$slots = collect($request->files)->map(fn ($meta) => [
'key' => $session->artifactKey($meta['checksum']),
'url' => Storage::disk('s3')->temporaryUploadUrl(
$session->artifactKey($meta['checksum']),
now()->addMinutes(10),
),
]);
// Ledger first, bytes later: a slot that never lands is auto-reaped.
$session->artifacts()->createMany(
$slots->map(fn ($s) => ['key' => $s['key'], 'status' => 'reserved'])
);
return response()->json(['slots' => $slots]);
}05 — the outcome
What the numbers say
- remote company, tracked & trusted
- 100%
- media bytes through the app server
- 0
- timelines instead of raw surveillance
- task-level
Remote stopped being a leap of faith — for management and for the team.