Files

6.4 KiB

Project memory — time_track_server

Handoff notes for agent conversations. Verify claims against current code and Git state before relying on them. Last updated: 2026-09-14.

Current focus

The combined-timesheet server is deployed and in daily use. Real data flows from both local trackers via systemd timers. Remaining feature work: mobile-provider PDF call parser (blocked on a redacted sample PDF), optional Ollama enrichment (design only so far).

Architecture

  • Outer repo (this one): FastAPI server (app/) + Docker Compose deployment + two Git submodules.
    • time_track/ — Waybar tracker (submodule, origin http://100.103.83.12:3003/fegger/waybar_time_track.git)
    • zed_time_tracker/ — Zed hours tracker (submodule, origin .../fegger/zed_time_tracker.git)
  • Server: app/main.py (routes), app/services.py (ingest/derivation/grouping), app/models.py (RawEvent, TimeEntry, CallImport), app/templates/ (Jinja + Pico CSS), app/database.py.
  • Access: http://100.103.83.12:3008 over Tailscale; BIND_IP from .env; Postgres internal-only; secrets only via .env (never commit .env).
  • Deploy on server: git pull && docker compose up --build -d (submodules first: commit each submodule, push, then update pointers in outer repo, push all three).
  • Backup: docker compose exec -T postgres pg_dump -U time_track time_track > backup.sql.

Data semantics (must preserve)

  • Timezone: day grouping, display, and monthly reports use Europe/Vienna (TZ in services.py); timestamps stored UTC-aware.
  • Sources: time_track (explicit intervals → accepted on ingest), zed (heartbeat-derived blocks → suggested), manual (web-created → accepted), commits stored as RawEvent(source="zed_commit"), displayed per day, never time.
  • Unique key (source, device_id, external_id); Zed block external_id = stable_id(device, project_slug, start, end) — heartbeat re-derivation must skip existing IDs so user accept/reject decisions stick (fixed 500 bug on 2026-09-14).
  • Overlap rule (user decision 2026-09-14): suggested entries overlapping accepted time are evidence only — "Accept day" skips them, UI hides Accept, POST /entries/{id}/status returns 422. Never let inferred time double-count.
  • Accepted zed_inferred becomes kind="manual" (durable, survives re-derivation).
  • Rejected entries are excluded from exports; suggested time always reported separately from accepted.
  • Monthly export GET /export/monthly?month=YYYY-MM&format=md|csv; full calendar month, Vienna dates, month-boundary-safe.
  • DATABASE_URL is built via SQLAlchemy URL.create from POSTGRES_* env parts — never go back to string interpolation (passwords with @ : / # % broke it).

Client sync (this workstation, verified working)

  • zed_time_tracker/sync.py (per project, offset-based state in .zed-hours/sync-state.json, strips absolute paths) and sync_all.py (multi-project wrapper reading ~/.config/zed-hours-sync/{env,projects.txt}).
  • time_track/time_track.py sync (re-sends all finished entries; server dedupes by entry UUID).
  • systemd user timers on this workstation (framework): zed-hours-sync.timer + time-track-sync.timer, every 15 min, Persistent=true; env files ~/.config/{zed-hours-sync,time-track-sync}/env hold real tokens (mode 600 — never print or commit them). Both verified syncing successfully on 2026-09-14 (time_track: 15 entries; zed: 0 new, state up to date).
  • Only project configured in projects.txt: /home/fegger/Code/ixsol/gem360-git (slug gem360-git).

Decisions & conventions

  • Single user; no rates/invoicing — duration reporting only; call logs are mobile-provider call-detail records (logs only, no audio).
  • Zed model choice for optional enrichment: qwen3:8b at http://100.103.83.12:11345 (env OLLAMA_* reserved; no Ollama code yet — deterministic parsing first).
  • Call import pipeline is upload + text extraction (pdfplumber, pypdf fallback, needs_ocr status) only; provider-specific parser intentionally deferred until a redacted sample PDF is provided.
  • Auth: single password (ADMIN_PASSWORD) for web, SYNC_API_TOKEN bearer for sync APIs; unauthenticated browser requests redirect to /login (303), APIs return 401.
  • Commits: repo hygiene — commit only when asked (user has approved commit+push each iteration); keep .env and tokens out of everything.

Open items / blockers

  • Call-log PDF parser: needs a redacted mobile-provider sample statement (columns/layout/duration format) before implementation. Upload UI already collects files to the call_imports volume.
  • Ollama enrichment: planned worker step for suggesting task/project from provider labels; output must be suggestion-only, strict JSON (Pydantic), minimal PII sent.
  • No HTTPS (Tailscale encryption only); add reverse proxy/Tailscale Serve if browser HTTPS is wanted.
  • User timers run only while the desktop session is active; loginctl enable-linger would extend to logout (not enabled — needs user decision).
  • Known harmless diagnostics: naive datetime.now() warnings in tracker scripts (local-time JSON format is deliberate), FastAPI Depends-default style warnings, broad except in PDF fallback (intentional), unsorted-import lints in main.py; stale "group_*_by_day not found" warnings in app/main.py are false positives (functions exist in services.py, exercised in smoke tests).
  • zed_time_tracker/ shows ? in git submodule status from an untracked __pycache__/ (regenerated each run; harmless). Note: time_track repo has a tracked __pycache__/time_track.cpython-314.pyc — do not delete it.

Files that matter now

  • app/main.py, app/services.py, app/models.py, app/schemas.py, app/templates/index.html
  • zed_time_tracker/sync.py, zed_time_tracker/sync_all.py, time_track/time_track.py (sync section ~L267)
  • docker-compose.yml, .env.example, README.md

Validation workflow (proven)

  1. python3 -m py_compile app/*.py time_track/time_track.py zed_time_tracker/*.py
  2. docker compose build / config --quiet with inline env vars (never create a local .env in the repo)
  3. Full smoke test on BIND_IP=127.0.0.1 with a hostile password (e.g. containing @ : # / %), seed data via sync APIs, exercise login + UI + exports with curl, verify idempotent re-syncs
  4. Always end with docker compose down -v --remove-orphans and remove temp files (cookies, reports)
  5. Commit submodule repos individually, update outer pointers, push all three