# Calls History Malaysia time design

Date: 2026-08-28

Status: approved

## Context

The production Calls History page currently displays an Android-agent
recording eight hours early. The observed row was:

```text
Kexin · Lim kean hon · 14s · 28 Aug 2026, 10:16 · Outbound
```

The recording occurred at approximately 18:16 Malaysia time. The agent upload
correctly normalizes its `started_at` instant to UTC before storing
`called_at`. The Calls History frontend already formats incoming timestamps in
`Asia/Kuala_Lumpur`, but the backend serializes the timezone-less database
value as though it were already Malaysia local time. The resulting ISO value
describes the wrong instant, so the frontend cannot recover the missing eight
hours.

## Goals

- Display every Calls History list and detail timestamp in Malaysia standard
  time, `Asia/Kuala_Lumpur` (UTC+8).
- Preserve UTC as the storage and API instant convention.
- Make the backend timezone explicit so display is correct regardless of the
  server, PHP, or browser timezone.
- Cover the real 10:16 UTC → 18:16 Malaysia regression with automated tests.

## Non-goals

- Do not rewrite historical database rows.
- Do not change Calls History sorting or date-filter semantics in this task.
- Do not redesign the Calls History page.
- Do not refactor unrelated date formatting across petaV3.
- Do not change recording-upload timestamps that are already normalized to
  UTC.

## Approaches considered

### A. Add eight hours in Vue

Rejected. The frontend already performs the correct timezone conversion. A
manual offset would double-shift correctly formed timestamps, mishandle other
zones, and hide the backend contract error.

### B. Store Malaysia wall-clock time in `called_at`

Rejected. This removes the instant's timezone semantics, conflicts with the
agent-upload UTC invariant, and makes cross-timezone consumers ambiguous.

### C. Serialize the UTC instant explicitly, then format in Malaysia time — selected

Keep the database convention as UTC. When shaping Calls History row and detail
payloads, interpret the stored `called_at` wall-clock as UTC and emit an ISO
8601 UTC value ending in `Z`. The existing client then formats that instant
using the configured `userTimezone`, whose fallback is
`Asia/Kuala_Lumpur`.

For the observed example:

```text
Database UTC: 2026-08-28 10:16:00
API instant:  2026-08-28T10:16:00Z
Page display: 28 Aug 2026, 18:16
```

## Data contract

The Calls History payload must follow these rules:

- `called_at` is either `null` or a complete ISO 8601 UTC instant.
- A database value `2026-08-28 10:16:00` is emitted as
  `2026-08-28T10:16:00Z`, not `2026-08-28T10:16:00+08:00`.
- The list-row transformer and `CallRecordingPresenter` summary/detail output
  use the same conversion rule.
- Other timestamps are unchanged unless they represent the same UTC storage
  invariant and are rendered by the same Calls History surface.

The implementation should use the smallest shared conversion already justified
by the list and presenter consumers. It must distinguish changing a timezone
label from converting an instant: the database wall-clock value is known UTC,
so it is assigned UTC semantics before ISO serialization.

## Presentation behavior

- Calls History list dates use the Inertia `userTimezone` value, falling back
  to `Asia/Kuala_Lumpur`, rather than a browser-local timezone.
- The Call detail workspace uses the same configured timezone.
- English display format remains unchanged apart from the corrected hour.
- The page does not show a literal `UTC+8` suffix unless the existing design
  already does so.

## Legacy-data safety

No migration is included. Before implementation is considered complete, the
targeted tests must prove the UTC convention for Android-agent uploads and the
Calls History serializer. Existing imported-call fixtures remain unchanged.
If a pre-existing source is discovered to store Malaysia wall-clock values
instead of UTC in the same column, that is a separate data-normalization issue
and must be reported rather than silently patched with source-specific offsets.

## Testing

### Backend feature/unit tests

- Seed a `called_at` database value of `2026-08-28 10:16:00` and assert the
  Calls History list prop emits `2026-08-28T10:16:00Z`.
- Request the same row as detail and assert the detail payload emits the same
  UTC instant.
- Preserve null timestamp behavior.
- Keep the agent-upload regression asserting that a `+08:00` upload timestamp
  is stored as its equivalent UTC wall-clock.

### Frontend tests

- Run the formatter under a non-Malaysia browser timezone and assert the UTC
  instant renders as `28 Aug 2026, 18:16` for
  `Asia/Kuala_Lumpur`.
- Assert the shared Call detail view renders the same Malaysia date/time.
- Confirm `userTimezone` is used with the Malaysia fallback.

### Relevant verification

- Run the targeted Calls History PHP feature tests.
- Run the targeted Vue/Vitest tests for the list and detail surfaces.
- Run the repository's relevant formatter/lint checks for touched PHP and Vue
  files.
- Re-check the production-like Kexin/Lim kean hon value locally through the
  rendered Inertia payload before deployment.

## Acceptance criteria

- The sample call displays `28 Aug 2026, 18:16` on both the Calls History list
  and detail view.
- The backend emits an unambiguous UTC ISO instant for `called_at`.
- Results do not depend on the browser's local timezone.
- No database migration or historical timestamp rewrite occurs.
- Sorting, filtering, call duration, direction, and other row fields remain
  unchanged.
