64 lines
5.4 KiB
Markdown
64 lines
5.4 KiB
Markdown
# Timelog
|
|||
|
|
|
||
A private, minimal time log with a duration-first timer, offline PWA, Forest CSV import/export, and time trends by day, week, month, year, and tag. The terminal-style UI offers Nord (the default) and Catppuccin Mocha and Latte themes, with the choice saved on each device. There is no tree or streak system.
|
|||
|
|||
|
|
## Run locally
|
||
|
|
|
||
|
|
Install [Bun](https://bun.sh/) and run:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
bun install
|
||
|
|
bun run build
|
||
|
|
```
|
||
|
|
|
||
|
|
In two terminals, run `bun run dev:api` and `bun run dev:web`, then open <http://localhost:3000>. Local development signs in automatically and binds the API to `127.0.0.1`. After editing the client, run `bun run build` again and reload. Local data is stored in browser IndexedDB and `dev.sqlite` (both ignored by Git).
|
||
|
|
|
||
|
|
`bun test` checks the supplied Forest export, CSV round-tripping, analytics, and sync behavior. `bun run typecheck` checks the TypeScript.
|
||
|
|
|
||
|
|
## Deploy on minhtrannhat.com
|
||
|
|
|
||
|
|
SQLite is an embedded database, so there is no useful separate SQLite server image. Compose runs two Bun images: a web server and an API that owns a SQLite file in the persistent `timelog_data` volume. Only the web container publishes a port, bound to server loopback at `127.0.0.1:8092`.
|
||
|
|
|
||
|
|
1. Create a [GitHub OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) with homepage `https://timelog.minhtrannhat.com` and callback `https://timelog.minhtrannhat.com/auth/github/callback`. Disable callback URL wildcard matching. This app asks for no OAuth scopes and uses your stable numeric GitHub user ID as its allowlist. You can find the ID at `https://api.github.com/users/YOUR_LOGIN`.
|
||
|
|
2. Copy `.env.example` to `.env` on the server and set `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, and `GITHUB_ALLOWED_ID`. Run `bun run vapid` once to generate push keys and put its three values in `.env`. Keep `.env` private (`chmod 600 .env`).
|
||
3. The repository is private, so the server reads it with its own read-only deploy key. Run `ssh-keygen -t ed25519 -N "" -C "vps deploy key: timelog" -f ~/.ssh/gitea_timelog` on the server, and add `~/.ssh/gitea_timelog.pub` in Gitea under the repository's **Settings → Deploy Keys** (leave write access off). Then clone the repository to `~/apps/timelog` and run `docker compose up -d --build` there:
|
|||
|
|
|
||
|
|
```sh
|
||
|
|
git clone -c core.sshCommand="ssh -i ~/.ssh/gitea_timelog -o IdentitiesOnly=yes" ssh://[email protected]:8089/minhtrannhat/timelog.git ~/apps/timelog
|
||
|
|
```
|
||
4. Install [deploy/nginx.conf](deploy/nginx.conf) as `/etc/nginx/sites-enabled/timelog.conf`, run `sudo nginx -t`, then `sudo systemctl reload nginx`. The server already has Nginx and Docker Compose; this config routes the subdomain to the loopback web port. Ensure the subdomain's DNS points through the existing Cloudflare setup before using the public URL.
|
|||
|
|
5. Sign in at `https://timelog.minhtrannhat.com`, open Settings, and import the Forest CSV. The supplied file has 2,709 sessions; re-imports skip identical records. Export there whenever you want a Forest-shaped copy of the log.
|
||
|
|
|
||
|
|
Do not publish the site before the OAuth credentials and allowed GitHub ID are set. The API rejects data requests without a valid session cookie and only accepts writes from `PUBLIC_ORIGIN`.
|
||
|
|
|
||
To deploy a new version, push it to Gitea, then pull and rebuild on the server. Back up the database first (see below).
|
|||
|
|
|
||
|
|
```sh
|
||
|
|
cd ~/apps/timelog
|
||
|
|
git pull --ff-only
|
||
|
|
docker compose up -d --build
|
||
|
|
```
|
||
|
|
|
||
|
|
Deploy only from pushed commits. Do not copy files from a local working tree to the server.
|
||
|
|
|
||
To back up the database consistently while it is running:
|
|||
|
|
|
||
|
|
```sh
|
||
|
|
cd ~/apps/timelog
|
||
|
|
docker compose exec -T api bun scripts/backup.ts /data/timelog-backup.sqlite
|
||
|
|
docker compose cp api:/data/timelog-backup.sqlite ./timelog-backup.sqlite
|
||
|
|
```
|
||
|
|
|
||
|
|
The backup destination must not already exist. Store the copied file somewhere outside the server too. The Forest CSV export is a convenient second copy of completed sessions, but the SQLite backup also retains sync, auth, and notification state.
|
||
|
|
|
||
|
|
## iPhone
|
||
|
|
|
||
|
|
Open the HTTPS site in Safari, then use **Share → Add to Home Screen**. The installed app works offline and syncs saved changes when it is online again. Open Settings in the installed app and enable completion notifications. Timelog sends standard Web Push to the iPhone subscription endpoint, which Apple routes through its push service; no Mac or Apple Developer account is needed. If the phone is offline at the deadline, the notification can arrive later when it reconnects, but not at the exact deadline. The next app open still completes the session at its chosen end time and caps the logged duration there.
|
||
|
|
|
||
|
|
No native iPhone widget is included. The PWA remembers the last tag and duration on each device.
|
||
|
|
|
||
|
|
## Data model
|
||
|
|
|
||
|
|
The timer writes actual elapsed time when stopped early and caps completed sessions at their selected duration. Manual add, edit, and delete live in the Log UI; signed-in devices exchange changes through the private sync API. The server is the durable copy, and each browser keeps an IndexedDB copy and outbox for offline work. Conflicting changes to the same entry follow last-write-wins order on the server.
|
||
|
|
|
||
|
|
Forest import/export uses exactly `Start Time,End Time,Tag,Note,Tree Type,Is Success`. Imported tree type and original timestamp strings are kept for export, but tree type is hidden from the UI. Trends split time across local-day boundaries and use actual time for stopped-early sessions. Analytics use the timezone of the device displaying them.
|