Skip to content

feat: persist goals and reading activity with derived progress - #12

Open
Eoic wants to merge 1 commit into
developmentfrom
feat/goals-redesign
Open

Eoic wants to merge 1 commit into
developmentfrom
feat/goals-redesign

Conversation

@Eoic

@Eoic Eoic commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Replace example goal/session responses with owned persisted goals and an immutable reading-activity ledger. Add calendar-aware projections for books, time, pages, and reading days; merge concurrent rules and activity, retain deletion history, and expose tracking schema version 1 through sync settings.

Testing

  • 39 focused route/service/migration tests pass; 12 aggregation/ownership tests also pass after the final history change.
  • Ruff, formatting, and Mypy pass.
  • Shared Dart/Python projection fixtures cover overlap, coverage deduplication, creation cutoffs, pauses, undo, midnight, DST, and shelf scope.
  • Migration test preserves an existing book and verifies upgraded metadata.

Compatibility and migration

Apply additive Alembic revision b5c6d7e8f901, update PostgreSQL publication/grants with the revised bootstrap, and deploy PowerSync streams before releasing the integrated client. Existing metric strings remain; reading_days and tracking_schema_version: 1 are additive. Older servers retain client-local tracking through staging.

Notes

Target development; versions remain unchanged. Coordinate with reader PR #5 and client #40. See docs-tracking.md for deployment order. Real multi-device PowerSync transport is an opt-in validation lane and has not been exercised by these isolated database tests.

Workspace integration #13 pins all three component revisions and documents release order.

Copilot AI balanced review requested due to automatic review settings October 6, 2026 14:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T14:30:08.261091Z 91a1612 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91a1612a0c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +52 to +53
if table == "reading_activities" and row is not None and schema.model_validate(row.payload) != parsed:
raise ValidationError("Activity and period history are immutable; append a correction")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject mutations to existing period snapshots

When a goal_periods row already exists, this immutability check is skipped, and the later validation only fixes goal_id and start_date. A PowerSync PUT/PATCH can therefore rewrite the period's target, end date, metric, or rule history, corrupting the retained history used by clients to reproject past progress. Include existing period rows in the immutable-payload comparison.

Useful? React with 👍 / 👎.

Comment thread papyrus/services/goals.py
Comment on lines +99 to +100
if definition.time_period != "custom":
definition.start_date, definition.end_date = calendar_period(definition, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor supplied dates for non-recurring goals

For every non-custom goal this overwrites the validated request dates with the calendar period containing now, even when is_recurring is false. For example, creating a one-time yearly goal for 2027 during 2026 persists and returns the 2026 period instead, silently activating the goal in the wrong interval. Calendar realignment should not replace the explicit dates of a non-recurring goal.

Useful? React with 👍 / 👎.

Comment thread papyrus/services/goals.py
Comment on lines +129 to +132
changes = request.model_dump(exclude_unset=True, exclude={"end_date"})
if changes.get("is_archived"):
changes["is_active"] = False
updated = definition.model_copy(update=changes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject or ignore nullable scalar goal updates

The request schema accepts explicit nulls for fields such as target_value, is_active, and is_archived, and exclude_unset=True retains those nulls here. model_copy then installs them without validation, after which constructing GoalRule raises an uncaught Pydantic validation error and the PATCH returns HTTP 500. These null values should either be rejected at request validation or omitted before building the rule.

Useful? React with 👍 / 👎.

title=title,
reading_time_minutes=values["seconds"] // 60,
pages_read=int(values["pages"]),
sessions_count=sum(entry.book_id == book_id for entry in counted),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count grouped sessions in per-book statistics

When a reader session is represented by multiple activity chunks sharing one session_id, the sessions endpoint groups those chunks and the total statistics count the identifier once, but this per-book field counts every chunk. The same response can consequently report one total session and multiple sessions for its only book; count distinct session_id or id values per book instead.

Useful? React with 👍 / 👎.

Comment on lines +267 to +268
sessions_count=len({entry.session_id or entry.id for entry in counted}),
average_session_minutes=total["seconds"] / 60 / len(counted) if counted else 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Divide average duration by grouped session count

For sessions split across multiple activity rows with the same session_id, sessions_count correctly deduplicates the identifier but the average divides total reading time by the number of activity rows. A single 20-minute session uploaded as two chunks is therefore reported as one session with a 10-minute average. Use the same distinct-session count as the denominator.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants