# ADR-070: All User-Facing Timestamps Displayed in IST (UTC+5:30)

## Status

Accepted, 2026-06-27.

## Status History

```yaml
status_history:
  - date: 2026-06-27
    status: Proposed
    changed_by: hkl
    reason: Formalising timezone enforcement pattern implemented across all PWAs
    changed_via: adr-kit (360lm)
  - date: 2026-06-27
    status: Accepted
    changed_by: hkl
    reason: IST pattern live across all 20 PWAs; prevents user confusion from UTC display
    changed_via: adr-kit (360lm)
```

## Context

The 360lm platform runs on a VPS with system clock set to UTC. PostgreSQL stores all timestamps in UTC by default (TIMESTAMP WITHOUT TIME ZONE). The field teams and administrators are located in India (IST = UTC+5:30).

Without explicit timezone conversion, PWAs could render UTC timestamps directly to users, causing a 5.5-hour offset that confuses field teams:
- A transaction logged at "2026-06-27 12:00 UTC" would display as "2026-06-27 12:00" to the user, when the actual local time was "2026-06-27 17:30 IST".
- Cron job logs and server-side events reported to users (deploy times, notification delivery, backup timestamps) would all appear in UTC.

Additionally, Claude (when reporting timestamps from server values or logs to the user in conversation) must always convert to IST so the user sees their local time, not server time.

The pattern is established: apply a 5.5-hour offset (`IST_MS = 5.5 * 60 * 60 * 1000 = 19,800,000 ms`) at the presentation layer (PWA JavaScript), never at the storage layer (PostgreSQL). This ensures:
- Timestamps remain in UTC in the database (schema integrity, time-arithmetic simplicity, cross-timezone queries).
- Conversion happens once, at display time, in the browser (client-side rendering).
- No PWA developer can accidentally forget to convert — it becomes a standard utility function and CSS locale.

## Decision

All timestamps shown to users (PWAs, server logs reported to users, Claude conversation context) are converted to IST (Asia/Kolkata, UTC+5:30) at the presentation layer only.

**Storage rule:** All timestamps are stored as UTC in PostgreSQL (TIMESTAMP WITHOUT TIME ZONE or TIMESTAMPTZ with UTC assumed). No schema changes.

**Display rule:** When rendering a timestamp to a user (in HTML, log output, or conversation), convert to IST using one of these patterns:

1. **JavaScript client-side (PWA):**
   ```javascript
   const IST_MS = 5.5 * 60 * 60 * 1000; // 19,800,000 ms
   const utcDate = new Date(utc_timestamp_ms);
   const istDate = new Date(utcDate.getTime() + IST_MS);
   element.textContent = istDate.toLocaleString('en-IN', {
     timeZone: 'Asia/Kolkata',
     year: 'numeric',
     month: '2-digit',
     day: '2-digit',
     hour: '2-digit',
     minute: '2-digit',
     second: '2-digit'
   });
   ```

2. **JavaScript (simpler, direct offset):**
   ```javascript
   const IST_MS = 19800000;
   const istTime = new Date(utc_timestamp_ms + IST_MS);
   console.log(istTime.toISOString()); // ISO string in IST offset
   ```

3. **PostgreSQL (for server-side reporting only):**
   ```sql
   SELECT created_at AT TIME ZONE 'Asia/Kolkata' AS created_ist
   FROM transactions;
   ```

4. **Claude conversation (when reporting timestamps):**
   Convert all UTC server values to IST before presenting to the user. Example:
   - Server reports: "backup completed at 2026-06-27T12:00:00Z"
   - Claude reports to user: "Backup completed at 2026-06-27 17:30 IST"

**Enforcement:** Every PWA must display timestamps in IST. Code review and Playwright tests must verify that no UTC times are rendered to users.

**Decision Maker:** hkl

## Implementation Notes

### Standard Timezone Offset Constant

Define in a shared utility file (e.g., `shared/time-utils.js`):

```javascript
// Time zone offset for IST (UTC+5:30)
export const IST_OFFSET_MS = 5.5 * 60 * 60 * 1000; // 19,800,000 ms

// Convert UTC timestamp to IST Date object
export function toIST(utcTimestamp) {
  const utcDate = new Date(utcTimestamp);
  return new Date(utcDate.getTime() + IST_OFFSET_MS);
}

// Format a UTC timestamp for display in IST
export function formatIST(utcTimestamp, options = {}) {
  const istDate = toIST(utcTimestamp);
  return istDate.toLocaleString('en-IN', {
    timeZone: 'Asia/Kolkata',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    ...options
  });
}
```

### Audit Pattern

When reviewing a PWA for IST compliance:

1. **Search for `.toLocaleString()` calls** — ensure `timeZone: 'Asia/Kolkata'` is present.
2. **Search for `.toISOString()` calls** — if the output is rendered to the user, verify it is IST-offset first.
3. **Search for `new Date()`** — if followed by `.toString()` or direct interpolation, ensure IST conversion happens first.
4. **Test in Playwright** — verify that a timestamp from the server (e.g., `2026-06-27T12:00:00Z`) is displayed as IST (e.g., `17:30`, not `12:00`).

### Claude's Reporting to Users

When Claude reports timestamps from server logs or database values in conversation:

1. Parse the UTC timestamp from the server output.
2. Apply the IST offset manually or via calculation: `IST = UTC + 5:30 hours`.
3. Report the IST time to the user with explicit timezone notation: "2026-06-27 17:30 IST".

Example:
- User asks: "When did the backup complete?"
- Server log shows: `2026-06-27T12:00:00Z`
- Claude converts: `12:00 UTC + 5:30 = 17:30 IST`
- Claude reports: "The backup completed at 2026-06-27 17:30 IST."

### Handling Daylight Saving Time

India does **not** observe Daylight Saving Time. IST is a fixed offset (UTC+5:30) year-round. No code changes are needed for seasonal transitions.

---

## Alternatives Considered

- **Store timestamps as IST in the database.** Rejected: breaks time-arithmetic queries, makes it impossible to calculate elapsed time across timezones, complicates inter-service timestamps (if another service uses UTC, comparison becomes error-prone). UTC storage is the industry standard.

- **Convert to IST at the API layer (PostgREST RPC).** Rejected: makes API responses timezone-specific, preventing code reuse across PWAs with different user timezones (if the platform ever expands to other countries). Conversion at the presentation layer (browser/Claude) is more flexible.

- **Use a JavaScript date library (e.g., date-fns, Day.js) with timezone support.** Rejected: adds a network dependency for a simple offset calculation. The 5-line utility function (above) is sufficient for IST and avoids external deps (ponytail: lazy mode).

- **Let each PWA define its own timezone offset constant.** Rejected: code duplication; if IST ever changes (unlikely), all PWAs must be updated. A shared utility enforces one source of truth.

- **Display both UTC and IST side-by-side.** Rejected: adds UI clutter and confuses non-technical users; IST-only is the right choice for field teams.

- **Use environment variable or config file for timezone.** Rejected: 360lm operates exclusively in India (IST). If multi-timezone support is needed in the future, a config can be added, but hardcoding IST now keeps the code simple (ponytail: upgrade trigger = support for multiple timezones).

---

## Consequences

**Positive:**
- Field teams see timestamps in their local time (IST), eliminating the 5.5-hour offset confusion.
- All timestamps in PostgreSQL remain in UTC, preserving time-arithmetic integrity and enabling future multi-timezone support if needed.
- Conversion happens at the browser layer, reducing server-side work.
- Claude always reports times in IST, keeping conversation context aligned with user reality.
- Audit trail is simple: every user-facing timestamp must use the IST utility function or `timeZone: 'Asia/Kolkata'`.

**Negative / Trade-offs:**
- Every PWA developer must remember to apply the IST conversion when rendering timestamps.
- Shared utility file (`time-utils.js`) must be maintained; if it is not present in a PWA, the developer must copy the pattern inline.
- Client-side timezone conversion assumes the browser's JavaScript engine supports `toLocaleString()` with `timeZone` parameter (supported in all modern browsers, but not IE11).

**Risks and mitigations:**
- **Developer forgets to convert a timestamp.** Mitigated by code review checklist (search for `.toLocaleString()`, `.toISOString()`, `new Date().toString()`) and Playwright tests that verify timestamps are not UTC.
- **Timestamp conversion is applied twice (once on server, once on client).** Mitigated by decision rule: convert only at the presentation layer, never in PostgREST RPCs.
- **A PWA receives an ISO string (e.g., `"2026-06-27T12:00:00Z"`) and forgets to parse it as a Date before converting.** Mitigated by requiring `new Date(iso_string)` before calling `toIST()`.
- **User's browser is set to a non-IST timezone.** Not mitigated — the browser's `timeZone: 'Asia/Kolkata'` parameter forces IST display regardless of browser locale. This is intentional: we want all users to see IST, not their browser's locale.

---

## Related Decisions

- **ADR-015** (Dev and Prod Are Two Full Stacks on the Same VPS) — both stacks run UTC system clocks; both PWAs must display IST.
- **ADR-014** (PostgREST Is the API Layer) — PostgREST returns timestamps in UTC (TIMESTAMP WITHOUT TIME ZONE in the schema); PWAs convert on receipt.
- **ADR-013** (Each PWA Is a Single Self-Contained HTML File) — timezone conversion happens in the PWA's JavaScript, not in a framework or backend.
- **Memory: Timezone IST** — user preference doc noting that all timestamps to Claude context must be IST-converted.

---

## References

- `memory/feedback_timezone.md` — user's timezone convention: all user-facing timestamps must be IST.
- `shared/time-utils.js` — shared utility for timezone conversion (to be created/maintained).
- `docs/adr/ADR-015-dev-prod-two-stacks-same-vps.md` — both stacks run on UTC.
- `docs/adr/ADR-014-postgrest-as-api-layer.md` — PostgREST returns UTC timestamps.
- MDN Web Docs: [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) — `timeZone` parameter documentation.
