Skip to content

Commit d135b5c

Browse files
committed
add progress history over time support
1 parent 0c58f02 commit d135b5c

14 files changed

Lines changed: 790 additions & 10 deletions

CLAUDE.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Reader Progress
2+
3+
KOReader-compatible reading progress sync server. Syncs reading position across devices (Kindle/KOReader, Readest on iOS/Android).
4+
5+
## Tech Stack
6+
- **Framework**: FastAPI + Uvicorn
7+
- **Language**: Python 3.12
8+
- **Local DB**: SQLite / PostgreSQL (SQLAlchemy)
9+
- **AWS DB**: DynamoDB (boto3)
10+
- **Auth**: HTTP headers (`x-auth-user`, `x-auth-key`) with bcrypt
11+
- **Testing**: Behave (BDD)
12+
- **Deploy**: AWS Lambda + CloudFront + DynamoDB via Terraform
13+
14+
## Local Development
15+
```bash
16+
python -m venv .venv && source .venv/bin/activate
17+
pip install -r requirements.txt
18+
uvicorn main:app --reload --port 8080
19+
# Swagger UI: http://localhost:8080/docs
20+
```
21+
22+
```bash
23+
# Or with Docker
24+
docker compose up -d
25+
```
26+
27+
## Testing
28+
```bash
29+
# Activate the virtual environment first, then run behave
30+
source .venv/bin/activate
31+
DB_BACKEND=sql behave
32+
33+
# Run a specific feature file
34+
DB_BACKEND=sql behave features/progress_history.feature
35+
```
36+
37+
## Deployment (AWS Lambda)
38+
```bash
39+
cd deployment
40+
./bootstrap.sh # one-time: creates S3 state bucket
41+
# copy and edit terraform/terraform.tfvars
42+
./deploy.sh # builds Lambda package + applies Terraform
43+
```
44+
45+
## Key Files
46+
- `main.py` — all FastAPI endpoints
47+
- `repositories/protocols.py` — database-agnostic interfaces
48+
- `repositories/sql.py` — SQLAlchemy implementation
49+
- `repositories/dynamodb.py` — DynamoDB implementation
50+
- `auth.py` — authentication helpers
51+
- `svg_card.py` — public progress card SVG renderer
52+
- `features/` — Behave BDD test scenarios
53+
54+
## Environment Variables
55+
- `DB_BACKEND``sql` (default) or `dynamodb`
56+
- `PASSWORD_SALT` — password hashing salt
57+
- `DATABASE_URL` — SQLite/PostgreSQL connection string
58+
- `DYNAMODB_USERS_TABLE`, `DYNAMODB_PROGRESS_TABLE` — table names
59+
- `RATE_LIMIT_ENABLED` — set to `false` during tests
60+
61+
## API Endpoints
62+
- `POST /users/create` — register
63+
- `GET /users/auth` — verify credentials
64+
- `PUT /syncs/progress` — upload progress
65+
- `GET /syncs/progress` — get progress for a document
66+
- `POST /documents/link` — link document hashes (same book, different formats)
67+
- `GET /books` — list all books with progress
68+
- `GET /card/{username}` — public SVG progress card

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ This creates:
114114
| `PASSWORD_SALT` | Salt for password hashing (set via Terraform) |
115115
| `DYNAMODB_USERS_TABLE` | Users table name (set via Terraform) |
116116
| `DYNAMODB_PROGRESS_TABLE` | Progress table name (set via Terraform) |
117+
| `DYNAMODB_PROGRESS_HISTORY_TABLE` | Progress history table name (set via Terraform) |
117118
| `AWS_REGION` | AWS region (set via Terraform) |
118119

119120
## KOReader Setup
@@ -325,6 +326,49 @@ curl -X PUT http://localhost:8080/syncs/progress \
325326
|--------|----------|
326327
| 200 | `{"status": "success"}` |
327328

329+
##### GET `/syncs/progress/{document}/history`
330+
331+
Return reading position at two points in time, useful for seeing how much was read during a session.
332+
333+
| Parameter | Type | Required | Description |
334+
|-----------|------|----------|-------------|
335+
| `document` | string | Yes (path) | MD5 hash of the document |
336+
| `start` | integer | Yes (query) | Unix timestamp for the start of the window |
337+
| `end` | integer | Yes (query) | Unix timestamp for the end of the window |
338+
339+
**At-or-before semantics:** Returns the most recent sync record whose timestamp is ≤ the requested time. If no sync occurred before a given timestamp, that snapshot's fields will be `null`.
340+
341+
```bash
342+
# What did I read today? (yesterday midnight → now)
343+
curl "http://localhost:8080/syncs/progress/0b229176d4e8db7f6d2b5a4952368d7a/history?start=1711065600&end=1711152000" \
344+
-H "x-auth-user: myuser" \
345+
-H "x-auth-key: a029d0df84eb5549c641e04a9ef389e5"
346+
```
347+
348+
**Response (200):**
349+
```json
350+
{
351+
"document": "0b229176d4e8db7f6d2b5a4952368d7a",
352+
"at_start": {
353+
"progress": "/body/DocFragment[10]/body/p[3]/text().0",
354+
"percentage": 0.31,
355+
"timestamp": 1711027200
356+
},
357+
"at_end": {
358+
"progress": "/body/DocFragment[22]/body/p[22]/text().1001",
359+
"percentage": 0.58,
360+
"timestamp": 1711148000
361+
}
362+
}
363+
```
364+
365+
If `at_start.timestamp == at_end.timestamp`, no reading occurred in the queried window. If either snapshot has `null` fields, no history record existed before that timestamp.
366+
367+
| Status | Response |
368+
|--------|----------|
369+
| 200 | History response with two snapshots |
370+
| 400 | `{"detail": "start must be <= end"}` |
371+
328372
##### GET `/syncs/progress/{document}`
329373

330374
Retrieve the latest progress for a document.

features/book_management.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Feature: Book Management
8383
When I request the SVG card for user "reader"
8484
Then the SVG response should succeed
8585
And the response content type should be "image/svg+xml"
86-
And the SVG should contain "Currently Reading"
86+
And the SVG should contain "In Progress"
8787

8888
Scenario: SVG card returns valid SVG for user with no books
8989
When I request the SVG card for user "reader"

features/progress_history.feature

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
Feature: Progress History
2+
As a KOReader user
3+
I want to query my reading progress at two points in time
4+
So that I can see how much I read during a session
5+
6+
Background:
7+
Given a user "reader" with password "readerpass" exists
8+
9+
Scenario: History records are created when progress is synced
10+
When user "reader" updates progress for document "bookA"
11+
| progress | /body/p[10] |
12+
| percentage | 0.10 |
13+
| device | Kindle |
14+
| device_id | k-001 |
15+
Then user "reader" should have history for document "bookA"
16+
17+
Scenario: Query returns progress at two points in time
18+
Given user "reader" has saved progress for document "book1"
19+
| progress | /body/p[10] |
20+
| percentage | 0.10 |
21+
| device | Kindle |
22+
| device_id | k-001 |
23+
And user "reader" updates progress for document "book1" after a delay
24+
| progress | /body/p[50] |
25+
| percentage | 0.50 |
26+
| device | Kindle |
27+
| device_id | k-001 |
28+
When user "reader" queries history for document "book1" around those two syncs
29+
Then at_start should show percentage 0.10
30+
And at_end should show percentage 0.50
31+
32+
Scenario: Returns null fields when no history exists before start timestamp
33+
Given user "reader" has saved progress for document "book2"
34+
| progress | /body/p[20] |
35+
| percentage | 0.20 |
36+
| device | Kindle |
37+
| device_id | k-001 |
38+
When user "reader" queries history for document "book2" with start=0 and end=1
39+
Then at_start should have null fields
40+
And at_end should have null fields
41+
42+
Scenario: No reading detected when queried window has no new syncs
43+
Given user "reader" has saved progress for document "book3"
44+
| progress | /body/p[10] |
45+
| percentage | 0.10 |
46+
| device | Kindle |
47+
| device_id | k-001 |
48+
When user "reader" queries history for document "book3" with a future window
49+
Then at_start and at_end should have the same timestamp
50+
51+
Scenario: History resolves linked document to canonical hash
52+
Given user "reader" has saved progress for document "hashA"
53+
| progress | /body/p[30] |
54+
| percentage | 0.30 |
55+
| device | Kindle |
56+
| device_id | k-001 |
57+
And user "reader" has linked documents "hashA" and "hashB"
58+
When user "reader" queries history for document "hashB" up to now
59+
Then at_end should have a non-null percentage
60+
61+
Scenario: start greater than end returns 400
62+
When user "reader" queries history for document "anydoc" with start=2000 and end=1000
63+
Then the request should fail with status 400
64+
65+
Scenario: All-books history endpoint returns records in range
66+
Given user "reader" has saved progress for document "chartbook"
67+
| progress | /body/p[10] |
68+
| percentage | 0.10 |
69+
| device | Kindle |
70+
| device_id | k-001 |
71+
When user "reader" queries all history with start=0 and end=9999999999
72+
Then the response should contain a book entry for document "chartbook"
73+
And that book entry should have at least 1 record

0 commit comments

Comments
 (0)