Repository navigation
Timesheets #4
Description
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.mdand 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.yamlentries + auth/pagination overlays for Clockify's Projects and Users/Members endpoints, following the existingtimeEntrythree-file pattern. - A2 (openapi-extensions): Decide on and add a foreign-key/reference vocabulary — either finish
spec/filtering's Link +x-for-eachproposal, or add a narrowerx-foreign-keyextension. Needed to formally describe that a time entry'sprojectId/userIdreference 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 existingclockifyProjectionlogic there and extending it to map Clockify Project/User records into our nativeProject/Personclasses viaAtomicLens/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
- Retire the server-side Clockify plugin in favor of LocalThought (no server-side code) atomic-server#1534 — Retire the server-side Clockify plugin in favor of LocalThought
- Clockify timesheet plugin: Phase A — Project/Person enrichment atomic-server#1601 — original filing of this plan, superseded by this issue
planning/clockify.md,planning/clockify-ui/*.dc.html(in atomic-server) — existing phased plan and UI mockups
🤖 Generated with Claude Code
Activity
[-]Clockify timesheet plugin: Phase A — Project/Person enrichment[/-][+]Timesheets[/+]michielbdejong commentedon Sep 21, 2026
Progress update:
referencesfield to CRUD Causality for exactly this case; Willow: encode entries and export Atomic signing candidates #164 was updated to use it.clockifyProjectionout of this repo's duplicated copy intodevonian/platform-lenses/clockify, plus a newresolveClockifyReferencesmatching each time entry'sprojectid/useridagainstproject/memberrecords fetched in the same batch. This repo's andontola/atomic-server'sintegrations/clockify/localthought.tsnow re-export from devonian instead of duplicating (branchclaude/hopeful-hawking-kqlrdyin both, pushed, no PR opened yet).browser/data-browser/package.json(inontola/atomic-server) pinsdevonianto a commit SHA, so the new export won't actually resolve until that pin is bumped past a commit containing Point Calendar README at the google-calendar OAD migration #24 — left as a deliberate follow-up once Point Calendar README at the google-calendar OAD migration #24 merges, same as thecatalog.jsonpin pattern inlocalthought/overlays.Generated by Claude Code
Add a Clockify platform lens (#24)
Delegate Clockify's schema lens to devonian (#1611)
TrekMeester batch 2026-09-25: storage simplification (7 PRs) (#1773)