Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test:
@# Note that extensive integration tests are also run in the bundle repo,
@# for both aw-server and aw-server-rust, but without code coverage.
python -c 'import aw_server'
python -m pytest tests/test_server.py tests/test_profile.py tests/test_profile_config.py tests/test_query_cache.py tests/test_import.py tests/test_host_header.py tests/test_database_concurrency.py
python -m pytest tests/test_server.py tests/test_profile.py tests/test_profile_config.py tests/test_query_cache.py tests/test_import.py tests/test_device_addressing.py tests/test_host_header.py tests/test_database_concurrency.py

typecheck:
python -m mypy aw_server tests --ignore-missing-imports
Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,66 @@ You can also get a very decent API browser by browsing to `localhost:5600` after

There are also some API examples in the [documentation](https://docs.activitywatch.net/en/latest/api.html).


## Draft device-addressed API

This branch is implementation work for ActivityWatch/activitywatch#302. It depends
on draft ActivityWatch/aw-core#183 (pinned in `pyproject.toml`), and includes the
local discovery commits from ActivityWatch/aw-server#183 rebased on master.
**Do not release this as a complete Python/Rust identity contract yet.**

With the SQLite backend, `/api/0/devices/<device_id>/buckets/<id>` and its
`events`, `events/count`, `events/<event_id>`, `events/delete`, `heartbeat`,
`export`, and `export/csv` operations use an explicit device scope. Device bucket
listing is `/api/0/devices/<device_id>/buckets/`; scoped queries use
`POST /api/0/devices/<device_id>/query`; scoped full export is
`GET /api/0/devices/<device_id>/export`. `local` and `/api/0/info.device_id`
normalize to the same stored local sentinel. Peer `hostname="!local"` is rejected.
Unsupported Memory/Peewee scoped operations return 501, never global fallback.

Legacy bucket creation is local-only. Other legacy name-addressed operations
retain unique global-name compatibility; ambiguous names return 409 before a
mutation/export response. Bidirectional hostname aliases preserve stored names
and histories. Exact names win. Legacy POST cannot acknowledge a local alias
when subsequent global writes would select another device. Query cache keys
include scope; heartbeat keys use device and resolved canonical bucket ID.
Metadata changes clear query and heartbeat state.

`GET /api/0/devices` includes the local UUID even when empty, plus stored peers.
Peer hostname is a deterministic representative of bucket metadata, not a
latest-hostname registry. `GET /api/0/devices/buckets` groups metadata explicitly;
`GET /api/0/devices/export` uses the same envelope with events:

```json
{"devices": {"source-uuid": {"buckets": {"window": {"id": "window", "device_id": "source-uuid", "type": "test", "client": "watcher", "hostname": "host", "created": "2024-01-01T00:00:00Z", "events": []}}}}}
```

`POST /api/0/devices/import` accepts that envelope, rejects empty or `local`
source identities, and validates enclosing ownership, bucket keys and event
payloads before writes. Local exported ownership becomes the source UUID, so
history stays peer-owned at another server. Existing same-device buckets merge
events and skip duplicates (timestamp, duration, data), matching Rust; an
alias of stored history merges into that row instead of creating a sibling.
Legacy `/import` still rejects existing exact names. Individual groups use
legacy rollback for buckets this call created; a runtime failure can leave
earlier successful groups.
Metadata `data` is preserved. Grouped export streams SQLite rows from one read
snapshot into a private temporary file before responding; failures cannot send
partial JSON with a success status. The response owns the staged file, not a
live database cursor. Concurrent WAL writes are excluded from that snapshot.
Only metadata and one event at a time are retained in memory; legacy exports
keep their existing implementation. No watcher adoption or sync-v2 import is
enabled by this draft.

Run `make test` for the normal fast server slice plus the device HTTP contract.
The pinned draft core is required; tests use temporary file-backed SQLite and
never a production database. Full shared HTTP parity and backend-specific
migration/same-name acceptance still gate removal of Rust's global name guards.

SQLite HTTP requests on worker threads use their own datastore/connection,
closed at request teardown; they share API write serialization, query cache and
pair-keyed heartbeat state, never a mutable scope. This preserves SQLite's
thread ownership check. Grouped export flushes under the write lock, then
stages from a dedicated read-only snapshot connection so later API writes are
not blocked. Event streaming retains the backend's existing time-range
semantics; unbounded archive/timestamp fidelity remains acceptance work.
Loading
Loading