Skip to content

Latest commit

 

History

History
172 lines (126 loc) · 8.2 KB

AGENTS.md

File metadata and controls

172 lines (126 loc) · 8.2 KB

Project Overview

Toki2 is a provider-agnostic time tracking and development workflow platform:

  • Track work time via pluggable time tracking providers (currently: Kleer)
  • Monitor pull requests across pluggable SCM providers (currently: Azure DevOps)
  • Real-time notifications for PR activity (comments, closures, mentions)
  • Generate time entry notes from work items linked to PRs

Design goal: The system is designed to support multiple time tracking backends (not just Kleer) and multiple PR/issue providers (not just Azure DevOps). All new backend features should be built with this provider-agnosticism in mind.

Tech Stack

Backend (Rust): Axum, SQLx (PostgreSQL), Tokio, azure_devops_rust_api, web-push Frontend (React/TS): Vite, TanStack Router + Query, Zustand, shadcn/ui, Tailwind Package manager: Bun — never use npm or npx, always use bun / bunx

Project Structure

toki-api/       # Main Axum backend
az-devops/      # Azure DevOps API wrapper crate
kleer/          # Kleer API client crate
app/            # React frontend
videos/         # fframes sources of product videos (standalone crates, outside the Cargo workspace)

Kleer Notes

Read .docs/kleer.md before Kleer-specific changes. It contains durable Kleer API URLs, service-account configuration, mapping rules, status/stat semantics, and implementation touchpoints.

Version Control

jj (Jujutsu) is preferred over git. This repo uses jj for version control. Use jj commands instead of git when possible.

Architecture Principles

Hexagonal architecture (ports & adapters) is the target pattern for all backend domain logic. When adding or modifying backend features:

  1. Domain logic must not depend on specific providers. Business logic lives in domain/services/ and depends only on traits defined in domain/ports/. Never import az_devops::* or kleer::* from domain code.
  2. Define ports (traits) for external interactions. Inbound ports in domain/ports/inbound/ define use cases. Outbound ports in domain/ports/outbound/ define what the domain needs from external systems.
  3. Adapters implement ports. Provider-specific code lives in adapters/outbound/{provider}/. HTTP handlers live in adapters/inbound/http/.
  4. Prefer extending existing traits over creating provider-specific shortcuts. If a new capability is needed, add it to the relevant port trait and implement it in the adapter.

Abstraction Status

Domain Status Notes
Time tracking Fully hexagonal TimeTrackingClient + TimeTrackingService ports, KleerAdapter
Work items Fully hexagonal WorkItemProvider + WorkItemService ports, AzureDevOpsWorkItemAdapter
Pull requests Not abstracted RepoDiffer directly uses az_devops::RepoClient — needs refactoring
Notifications Not abstracted Coupled to PR change events

When working on partially or non-abstracted domains, prefer moving toward the hexagonal pattern rather than adding more provider coupling.

Key Patterns

Backend

  • Repository pattern: Database access via traits (UserRepository, etc.) with *Impl implementations
  • AppState: Shared state container passed via Axum extractors
  • RepoDiffer workers: Background tasks polling ADO for PR changes, communicating via mpsc channels (note: tightly coupled to Azure DevOps, future refactoring target)
  • SQLx offline mode: .sqlx/ caches query metadata. Set SQLX_OFFLINE=true to compile without a live DB. Run cargo sqlx prepare after changing SQL queries

Time Tracking Architecture (Hexagonal)

The time tracking system is the reference implementation of the hexagonal pattern. New domains should follow this structure:

toki-api/src/
├── domain/
│   ├── models/           # Domain types (ActiveTimer, TimeEntry, Project, etc.)
│   ├── ports/
│   │   ├── inbound/      # TimeTrackingService trait (use cases)
│   │   └── outbound/     # TimeTrackingClient, TimerHistoryRepository traits
│   ├── services/         # TimeTrackingServiceImpl (business logic)
│   └── error.rs          # TimeTrackingError
└── adapters/
    ├── inbound/http/     # TimeTrackingServiceFactory, HTTP response types
    └── outbound/
        ├── kleer/        # KleerAdapter (implements TimeTrackingClient)
        └── postgres/     # PostgresTimerHistoryAdapter (implements TimerHistoryRepository)

Key traits:

  • TimeTrackingClient (outbound): Interface for time tracking providers (timer, projects, calendar)
  • TimerHistoryRepository (outbound): Interface for local timer history storage
  • TimeTrackingService (inbound): Use cases for HTTP handlers
  • TimeTrackingServiceFactory: Creates service instances for authenticated Toki users

Per-request service creation: The service is created per request from the authenticated Toki user and the local Kleer mapping. Kleer credentials are server-side service-account config, not browser cookies or frontend state. The service merges provider data with local timer history for accurate start/end times.

Frontend

  • File-based routing: TanStack Router (_layout/ for layouts, $param for dynamic routes)
  • Query factories: Queries in factory objects returning queryOptions()
  • Co-located components: Route-specific components in -components/ directories
  • Props inline: Inline props in function signatures, prefer type over interface

Development

Use just to run common commands (run just to see all available recipes):

just dev        # Run both backend and frontend

# Or individually:
just init-db    # Initialize database (first time setup)
just run        # Run backend
just app        # Run frontend dev server

Verifying Changes

just check-all  # Verify everything (backend + frontend)

# Or individually:
just check      # Backend - verify Rust compiles
just tsc        # Frontend - TypeScript check
just lint       # Frontend - ESLint

# Without a running database, use SQLX_OFFLINE=true:
SQLX_OFFLINE=true just check

Database tests (#[sqlx::test]) create and drop a database per test on the server in DATABASE_URL. Point it at a disposable local Postgres, never a shared or production one:

SQLX_OFFLINE=true DATABASE_URL=postgres://postgres:password@localhost:5433/postgres cargo test -p toki-api

Observability

just dev starts a standalone Aspire dashboard at http://127.0.0.1:18888 and exports OTEL over gRPC to http://127.0.0.1:18889.

When inspecting traces from the standalone dashboard, query it directly with --dashboard-url; aspire ps may report no AppHost because this repo runs the dashboard without an Aspire AppHost:

# A short trace id prefix works for trace lookup and returns the full trace id.
aspire otel traces \
  --dashboard-url http://127.0.0.1:18888 \
  --trace-id 3e7db67 \
  --format Json \
  --limit 20 \
  --non-interactive \
  --nologo

# Span lookup needs the full trace id from the trace result.
aspire otel spans \
  --dashboard-url http://127.0.0.1:18888 \
  --trace-id 3e7db67019bfc8a3b46bbdde72a791af \
  --format Json \
  --limit 200 \
  --non-interactive \
  --nologo

Important Notes

  1. Provider-agnostic by default - New backend features should use ports/adapters, not couple directly to Kleer or Azure DevOps
  2. Kleer integration uses a service account - normal users must be locally mapped to Kleer users; read .docs/kleer.md for Kleer-specific changes
  3. Minimal test coverage - be careful with changes
  4. Coordinated changes - Backend API changes typically require frontend updates
  5. Route generation - Don't edit routeTree.gen.ts manually
  6. shadcn/ui - Components in app/src/components/ui/ are from shadcn/ui
  7. Timer buddy sprites are generated - app/public/buddy/ and app/src/lib/buddy/species.generated.ts come from app/scripts/build_buddy_sprites.py (PMD SpriteCollab, CC BY-NC 4.0). Regenerate them with the script instead of editing by hand, and keep CREDITS.md with them

Configuration

Backend config: toki-api/config/{base,local,production}.yaml + TOKI_* env vars
Required secrets: Azure AD OAuth credentials, Kleer integration token and company id