# Time and Timezones in OpenGit

OpenGit stores and transmits all timestamps in **UTC**. The client displays them in the user's local timezone using the browser's system settings.

---

## Design Principles

1. **Store in UTC** – The database and server logic use UTC only.
2. **Transmit as ISO 8601 with Z** – API responses send timestamps like `2026-03-16T23:19:21.715Z`.
3. **Display in user's local time** – The browser converts UTC to local time via `toLocaleString()` and similar.

---

## Backend

### Storage

- **SQLite:** `CURRENT_TIMESTAMP` is UTC.
- **PostgreSQL:** Each connection runs `SET timezone = 'UTC'`, so `CURRENT_TIMESTAMP` is UTC.
- **Explicit inserts:** For `credits_history` and other critical tables, `created_at` is set with `utc_now_db()` (Python `datetime.now(timezone.utc)`) instead of relying on DB defaults, so inserts are always UTC regardless of server timezone.

### API Responses

All timestamps in JSON APIs go through `to_iso8601_utc()` (in `database.py`), which:

- Accepts `datetime`, `date`, or ISO-like strings
- Treats naive values as UTC
- Returns strings like `"2026-03-16T23:19:21.000Z"`

**Endpoints using `to_iso8601_utc`:**

- Credits history, notifications, feed, issues, tasks
- Admin (reports, backups, trusted IPs, banned_until)
- Branches, repo creation date, commit timestamps
- Shell history, profile contribution grid

---

## Frontend

### Parsing Timestamps

The client receives ISO 8601 strings with `Z`. For example:

```javascript
new Date("2026-03-16T23:19:21.715Z")
```

This parses as UTC; the `Date` object holds the correct instant.

### Display

```javascript
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
date.toLocaleDateString([], { month: 'short', day: '2-digit', year: 'numeric' })
date.toLocaleString()
```

The browser converts the UTC instant to the user's local timezone automatically.

### Fallback for Legacy Data

If the backend ever sends a timestamp without timezone (e.g. `"2026-03-16 19:15:00"`), the credits history uses `parseAsUTC()` to append `Z` before parsing, so it is treated as UTC and displayed correctly.

---

## Daily Credits

Daily credits reset at **UTC midnight** (00:00 UTC), not the user's local midnight.

- `last_credit_refresh` is compared with `datetime.now(timezone.utc).date()`
- If `last_refresh` is `None` or older than today (UTC), credits are granted
- Writes use `utc_now_db()` for explicit UTC timestamps

---

## Utilities

| Function | Purpose |
|----------|---------|
| `utc_now_db()` | Current UTC as `YYYY-MM-DD HH:MM:SS` for DB inserts |
| `to_iso8601_utc(dt)` | Convert a datetime/date/string to ISO 8601 with `Z` for API responses |

Both are defined in `database.py`.

---

## Server-Rendered Timestamps

- **Jinja `timesince` filter** – Relative strings like "2 hours ago" are timezone-agnostic and unchanged.
- **Date-only displays** (e.g. "Joined Mar 2025") – Rendered server-side; exact local time is less critical.

---

## Troubleshooting

### Wrong displayed time

1. **Backend:** Ensure `to_iso8601_utc()` is used for all timestamps in API responses.
2. **Frontend:** Ensure `new Date(...)` receives ISO 8601 strings ending with `Z` (or `+00:00`).
3. **Database:** For PostgreSQL, verify `SET timezone = 'UTC'` runs on each connection. For new inserts, use `utc_now_db()` where appropriate.

### Debugging

Set `window._debugTimestamps = true` in the browser console, then open the credits modal. Console logs will show:

- Raw `created_at` values
- Whether the string has timezone info
- The parsed Date and its local string representation
