d8366f5c255a15d573e140bb840849fe9180f4e7
- Migration 003: adds logged_at to sync_log for TTL pruning; migrates settings_history to UUID TEXT PK with updated_at column - SyncStore: Prune() deletes rows older than 30d and writes a '_pruned' marker at the boundary version; Pull() calls Prune lazily and returns ErrSyncStale (410) when the client's since_version is behind the marker - sync_handler.go: GET /api/sync/pull?since=N; POST /api/sync/push with last-updated_at-wins conflict resolution for entries, balance_adjustments, settings_history; closed_days/closed_weeks skipped (server-only mutations) - router.go: passes entryStore, adjustmentStore, settingsStore to SyncHandler - settings_store.go: UUID PK, updated_at column, Upsert() for push path - settings_service.go: generates UUID on create, sets updated_at on update - settings_handler.go: ID params changed from int64 to string - domain.go: Settings.ID string, Settings.UpdatedAt added - client.ts: all mutation methods catch TypeError (offline) and fall back to Dexie write + outbox enqueue; crypto.randomUUID() for offline creates; Settings.id type changed to string - db.ts: Dexie v3 — settings_history key path changed to string UUID; upgrade handler clears table for repopulation via pull - sync.ts: real pushOutbox to POST /api/sync/push; pullChanges uses GET with ?since=N; 410 triggers coldStart() + retry; coldStart() wipes all tables and resets last_version - 4 new Go store tests covering normal pull, stale client, empty prune, client-ahead-of-marker; all tests pass (store + service, 19 Vitest)
Wotra — Working Time Tracker
A self-hosted working time tracker with a Go backend and a Svelte PWA frontend. Offline-capable, single-binary deploy.
Features
- Start / Stop time tracking with an optional note.
- Close days: merges all entries (worked_ms = sum of durations, breaks excluded).
- Mark days as holiday, vacation, or sick (auto-credits expected daily hours).
- Close weeks: computes overtime/undertime against a frozen snapshot of your expected hours.
- Settings history: change hours/week with an
effective_fromdate — past closed weeks are unaffected. - PWA + offline: works without a network connection; syncs via IndexedDB outbox when back online.
- CSV export: entries, days, weeks.
- Midnight guard: running entries are automatically stopped at 23:59:59 if they cross midnight.
Quick Start
Tool versions are declared in mise.toml. Install mise and run mise install once to get the correct Go and Node versions.
Development
# Install frontend dependencies (first time only)
mise run install
# Start both servers (Go API + Vite dev server)
mise run dev
# Go API: http://localhost:8080
# Vite UI: http://localhost:5173 (with /api proxy)
AUTH_TOKEN defaults to devtoken in mise.toml. Override it by setting the variable in your environment before running.
Open http://localhost:5173, go to Settings, enter your token.
Production (single binary)
mise run build # builds web/ then embeds it in the Go binary
AUTH_TOKEN=mysecrettoken ./wotra
# open http://localhost:8080
All tasks
| Task | Description |
|---|---|
mise run build |
Build production binary (web + Go) |
mise run build:web |
Build Svelte frontend only |
mise run build:go |
Build Go binary only |
mise run dev |
Start API + UI dev servers concurrently |
mise run dev:api |
Start Go API server only |
mise run dev:ui |
Start Vite dev server only |
mise run test |
Run all Go tests |
mise run install |
Install frontend npm dependencies |
mise run clean |
Remove build artifacts |
Configuration (environment variables)
| Variable | Default | Description |
|---|---|---|
AUTH_TOKEN |
required | Bearer token for API access |
PORT |
8080 |
HTTP port |
DB_PATH |
wotra.db |
SQLite database file path |
TZ |
UTC |
Timezone for day/week key calculation |
API Reference
All API endpoints require Authorization: Bearer <token> except /healthz.
Entries
| Method | Path | Description |
|---|---|---|
| POST | /api/entries/start |
Start tracking |
| POST | /api/entries/{id}/stop |
Stop a specific entry |
| GET | /api/entries?from=&to= |
List entries by date |
| PUT | /api/entries/{id} |
Edit start/end/note |
| DELETE | /api/entries/{id} |
Soft-delete an entry |
Days
| Method | Path | Description |
|---|---|---|
| GET | /api/days?from=&to= |
List closed days |
| POST | /api/days/{day}/close |
Close a day (merge entries) |
| POST | /api/days/{day}/mark |
Mark as holiday/vacation/sick |
| DELETE | /api/days/{day}/close |
Reopen a closed day |
Weeks
| Method | Path | Description |
|---|---|---|
| GET | /api/weeks?from=&to= |
List closed weeks |
| POST | /api/weeks/{week}/close |
Close a week (compute overtime) |
| DELETE | /api/weeks/{week}/close |
Reopen a closed week |
Week key format: YYYY-Www (e.g. 2024-W03).
Settings
| Method | Path | Description |
|---|---|---|
| GET | /api/settings |
Current effective settings |
| PUT | /api/settings |
Add a new settings version |
| GET | /api/settings/history |
All settings history |
Export
| Method | Path | Description |
|---|---|---|
| GET | /api/export/entries.csv |
Export entries as CSV |
| GET | /api/export/days.csv |
Export days as CSV |
| GET | /api/export/weeks.csv |
Export weeks as CSV |
Sync
| Method | Path | Description |
|---|---|---|
| POST | /api/sync/pull |
Pull changes since a version |
| POST | /api/sync/push |
Push local changes (advisory) |
Health
| Method | Path | Description |
|---|---|---|
| GET | /healthz |
Unauthenticated health |
Architecture
┌─────────────────────────┐ ┌──────────────────────────┐
│ Svelte PWA (client) │ ◄─────► │ Go service (API) │
│ - IndexedDB (Dexie) │ HTTPS │ - REST/JSON endpoints │
│ - Service Worker │ JSON │ - Business logic │
│ - Sync outbox │ │ - SQLite (modernc) │
└─────────────────────────┘ └──────────────────────────┘
See PLAN.md for the full design document.
Development Notes
- Go module:
github.com/wotra/wotra - SQLite driver:
modernc.org/sqlite(pure Go, no CGO) - Day boundary: entries must not cross midnight; the server auto-stops any running entry at 23:59:59 local time.
- Settings history:
effective_fromlets you change hours/week without rewriting past closed weeks. - Overtime: stored as a signed
delta_mson eachclosed_weeksrow. Frozen at close time.
Description
Languages
Go
56.8%
Svelte
28.4%
TypeScript
14%
Dockerfile
0.5%
JavaScript
0.2%
Other
0.1%