Skip to content

Timesheets #4

Closed
Closed

Description

@michielbdejong

Context

The Clockify LocalThought integration (see ontola/atomic-server#1534, planning/clockify.md in atomic-server) currently imports time entries one-way, polling every 5 minutes. It works, but imported entries only carry Clockify's raw projectId/taskId/userId strings — there's no way to see which project or person an entry belongs to, and no reporting/breakdown by project or client.

This issue tracks Phase A: Project/Person enrichment — resolving those raw ids into linked, named Atomic Project/Person records, so the Timer view and reports can show real names instead of opaque ids.

Two explicit scoping decisions carried into this plan:

  • Webhooks are out of scope here. Clockify does support webhooks (NEW_TIME_ENTRY, TIME_ENTRY_UPDATED, TIME_ENTRY_DELETED, etc.), but adopting them means an admin-level API key (vs. the personal key LocalThought asks for today) and a public HTTPS receiver on our side — a bigger architectural change than this phase needs. We're sticking with polling for now; webhooks are tracked as a separate future project.
  • No migration path needed. There are zero live users of the old server-side Clockify plugin, so Phase A doesn't need to carry any legacy-install migration logic (this had been listed as a "v1 reduction" in planning/clockify.md and in Retire the server-side Clockify plugin in favor of LocalThought (no server-side code) atomic-server#1534 item 4 — it can just be dropped, not deferred).

Where this issue lives: filed here in atomic-plugins because this is where the Clockify integration work is happening now. Note atomic-plugins/README.md currently describes this repo as a temporary CI mirror pointing back to atomic-server/integrations as the source of truth — flagging that in case it needs updating to match.

Repo split for this work

Per the intended architecture, work is split by responsibility rather than landing entirely in one place:

Repo Responsibility Phase A work
localthought/overlays Describes how the Clockify API actually works (OpenAPI + overlays) Describe Clockify's Projects (GET /v1/workspaces/{workspaceId}/projects) and Users/Members (GET /v1/workspaces/{workspaceId}/users) endpoints
pondersource/openapi-extensions OpenAPI extension vocabulary used by overlays Add (or finish) a way to express "this field is a foreign key into another resource's collection"
localthought/syncables Generic syncing engine Add capability to fetch a secondary/referenced collection based on ids seen in an already-fetched record
localthought/devonian Schema lenses: external schema ↔ our native schema Build a real Clockify lens, using it to map Project/User → our native Project/Person classes
localthought/reflector Embeds syncables+devonian into atomic-server Not currently wired into atomic-server/atomic-plugins at all — see open question below
atomic-plugins (integrations/clockify/) + atomic-server browser/data-browser/.../PluginRuns/ UI and business logic of the timesheet app Consume enriched records: show project/person names in the Timer view, update the Manage panel's field-mapping table

What we found investigating the current state (with file:line citations)

overlays: Clockify's overlay footprint today only covers the timeEntry resource (auth-overlay.yaml, pagination-overlay.yaml, crud-causality-overlay.yaml, all in clockify.me/1.0.0-readonly/), and the vendored base OpenAPI doc it overlays only contains the two time-entries paths — Projects/Users don't exist anywhere in the pipeline yet, not even undescribed-but-present in the base spec. Good news: both syncables' and reflector's overlay appliers do additive deep-merges that can introduce brand-new paths/schemas without touching the upstream base doc, so this is pure additive overlay work following the exact same three-file pattern already used for timeEntry.

openapi-extensions: no finished extension (Pagination Schemes, CRUD Causality, Authenticated Principal) has a foreign-key/reference construct. The closest thing is an unpublished draft, spec/filtering/README.md (0.1.0-draft, no reference implementation), which sketches standard OpenAPI Link Objects plus a proposed x-for-each annotation for one-response-item→one-target-object-per-item cases. It's not implemented by any consumer and has no worked example of Clockify's actual shape (many time entries → one of a smaller set of projects, i.e. many-to-one from the referencing side). We need to decide: finish that draft, or add a narrower, purpose-built x-foreign-key/x-reference extension.

syncables: has no dependency-aware multi-collection fetch orchestration — performSync() fetches each discovered collection independently, with no "collect ids referenced by record X, then fetch collection Y" step. Also worth noting: syncables' own bundled overlay parser (src/openapi/overlay.ts) rejects bracket-JSONPath targets ($.paths['/v1/...']...), which is exactly the syntax the real Clockify overlays use — only reflector wrote a parser capable of applying them. Whichever repo ends up doing the secondary fetch, it'll need to either use reflector's overlay applier or fix syncables' own.

devonian: has the right primitives already — AtomicLens/AtomicIdentityMap/AtomicConnector, with AtomicProjection.related supporting multi-resource writes and stable identity allocation from one lens call (see examples/AtomicExtractEntity.ts for the closest existing pattern: mapping a flattened record with an inline foreign id into a linked entity). Caveat: that example's input already has the related entity's display fields inline; Clockify's time-entries payload only has bare ids, so the lens gives us the linking/writing half but not the "go fetch the name from Clockify's Projects/Users endpoint" half — that fetch has to happen upstream of the lens. There is currently no Clockify lens in devonian at all — only github-issues and google-calendar lenses exist there. The current Clockify projection (integrations/clockify/localthought.ts) is a parallel, duplicated implementation against a locally-declared type triple that's structurally identical to devonian's canonical FetchedPlatform types but not derived from them. As part of this phase we should migrate that projection into devonian/platform-lenses/clockify/lens/, mirroring the github-issues/google-calendar precedent, rather than continuing to extend the local copy in place.

reflector: confirmed not wired into atomic-server or atomic-plugins today — an earlier attempt to route imports through reflector was explicitly abandoned (planning/api-plugins.md in atomic-server) in favor of the current plugin model. It does solve a piece of the "secondary fetch" problem (discoverResourceModel/providerFor in src/sync/resources.ts), but only for parent-enumerates-children cases (e.g. list calendars, then list events per calendar) — the opposite cardinality from what we need (resolve a foreign-key field value against a sibling collection). Open question for this issue: do we re-establish reflector wiring as part of Phase A, or keep targeting the current plugin-model path directly and treat reflector integration as a separate, later decision? Recommend the latter for Phase A, to avoid conflating two large changes.

Task breakdown

  • A1 (overlays): Add crud-causality-overlay.yaml entries + auth/pagination overlays for Clockify's Projects and Users/Members endpoints, following the existing timeEntry three-file pattern.
  • A2 (openapi-extensions): Decide on and add a foreign-key/reference vocabulary — either finish spec/filtering's Link + x-for-each proposal, or add a narrower x-foreign-key extension. Needed to formally describe that a time entry's projectId/userId reference the new Project/User resources.
  • A3 (syncables, or wherever the fetch actually lands): Add the capability to fetch a secondary collection based on ids referenced in an already-fetched record. Needs a decision on whether this lands in syncables proper, or — since reflector isn't wired in and syncables' overlay parser can't yet read real overlays — is implemented pragmatically in the plugin integration layer for this phase, with the generic version tracked separately.
  • A4 (devonian): Build platform-lenses/clockify/lens/, migrating the existing clockifyProjection logic there and extending it to map Clockify Project/User records into our native Project/Person classes via AtomicLens/AtomicIdentityMap.
  • A5 (plugin): Wire the enriched, linked records into the Timer/table views (show project/person names, not raw ids) and update the Manage panel's field-mapping table (currently shows "Project · Task → Not mapped (raw ids kept on the row)").

Non-goals for this phase

Two-way sync, conflict resolution, row-menu push/discard/delete actions, tags/tasks/rates/custom fields, invoicing-style reporting, and webhook-based push updates all remain out of scope — tracked separately (two-way sync work is the existing unchecked item in planning/clockify.md; webhooks are a distinct future project per the scoping decision above).

Related


🤖 Generated with Claude Code

https://claude.ai/code/session_01Cng3r2ijbx8fBVZjLsEdH9

Activity

  1. changed the title [-]Clockify timesheet plugin: Phase A — Project/Person enrichment[/-] [+]Timesheets[/+] on Sep 21, 2026
  2. michielbdejong commented on Sep 21, 2026

    @michielbdejong
    ContributorAuthor

    Progress update:


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions