Files
chore/.github/specs/archive/feat-dynamic-points/feat-dynamic-points.md
Ryan Kegel 401c21ad82
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s
feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
- Implemented PendingRewardDialog for handling pending reward requests.
- Created RewardConfirmDialog for confirming reward redemption.
- Developed TaskConfirmDialog for task confirmation with child name display.

test: add unit tests for ChildView and ParentView components

- Added comprehensive tests for ChildView including task triggering and SSE event handling.
- Implemented tests for ParentView focusing on override modal and SSE event management.

test: add ScrollingList component tests

- Created tests for ScrollingList to verify item fetching, loading states, and custom item classes.
- Included tests for two-step click interactions and edit button display logic.
- Moved toward hashed passwords.
2026-02-10 20:21:05 -05:00

22 KiB

Feature: Dynamic Point and Cost Customization

Overview

Goal: Allow parents to customize the point value of tasks/penalties and the cost of rewards on a per-child basis after assignment.

User Story:
As a parent, I want to assign different point values to the same task for different children, so I can tailor rewards to each child's needs and motivations. For example, "Clean Room" might be worth 10 points for one child but 5 points for another.

Process:

  1. Assignment First: Tasks, penalties, and rewards must be assigned to a child before their points/cost can be customized.
  2. Edit Button Access: After the first click on an item in ScrollingList (when it centers), an edit button appears in the corner (34x34px, using edit.png icon).
  3. Modal Customization: Clicking the edit button opens a modal with a number input field allowing values from 0 to 10000.
  4. Default Values: The field defaults to the last user-set value or the entity's default points/cost if never customized.
  5. Visual Indicator: Items with custom values show a ✏️ emoji badge next to the points/cost number.
  6. Activation Behavior: The second click on an item activates it (triggers task/reward), not the first click.

Architecture Decisions:

  • Storage: Use a separate child_overrides.json table (not embedded in child model) to store per-child customizations.
  • Lifecycle: Overrides reset to default when a child is unassigned from a task/reward. Overrides are deleted when the entity or child is deleted (cascade).
  • Validation: Allow 0 points/cost (not minimum 1). Disable save button on invalid input (empty, negative, >10000).
  • UI Flow: First click centers item and shows edit button. Second click activates entity. Edit button opens modal for customization.

UI:

  • Before first click: feat-dynamic-points-before.png
  • After first click: feat-dynamic-points-after.png
  • Edit button icon: frontend/vue-app/public/edit.png (34x34px)
  • Button position: Corner of ScrollingList item, not interfering with text
  • Badge: ✏️ emoji displayed next to points/cost number when override exists

Configuration

No new configuration required. Range validation (0-10000) is hardcoded per requirements.


Data Model Changes

New Model: ChildOverride

Python (backend/models/child_override.py):

Create a dataclass that inherits from BaseModel with the following fields:

  • child_id (str): ID of the child this override applies to
  • entity_id (str): ID of the task/penalty/reward being customized
  • entity_type (Literal['task', 'reward']): Type of entity
  • custom_value (int): Custom points or cost value

Validation requirements:

  • custom_value must be between 0 and 10000 (inclusive)
  • entity_type must be either 'task' or 'reward'
  • Include __post_init__ method to enforce these validations
  • Include static factory method create_override() that accepts the four main fields and returns a new instance

TypeScript (frontend/vue-app/src/common/models.ts):

Create an interface with 1:1 parity to the Python model:

  • Define EntityType as a union type: 'task' | 'reward'
  • Include all fields: id, child_id, entity_id, entity_type, custom_value, created_at, updated_at
  • All string fields except custom_value which is number

Database Table

New Table: child_overrides.json

Indexes:

  • child_id (for lookup by child)
  • entity_id (for lookup by task/reward)
  • Composite (child_id, entity_id) (for uniqueness constraint)

Database Helper (backend/db/child_overrides.py):

Create database helper functions using TinyDB and the child_overrides_db table:

  • insert_override(override): Insert or update (upsert) based on composite key (child_id, entity_id). Only one override allowed per child-entity pair.
  • get_override(child_id, entity_id): Return Optional[ChildOverride] for a specific child and entity combination
  • get_overrides_for_child(child_id): Return List[ChildOverride] for all overrides belonging to a child
  • delete_override(child_id, entity_id): Delete specific override, return bool indicating success
  • delete_overrides_for_child(child_id): Delete all overrides for a child, return count deleted
  • delete_overrides_for_entity(entity_id): Delete all overrides for an entity, return count deleted

All functions should use from_dict() and to_dict() for model serialization.


SSE Events

1. child_override_set

Emitted When: A parent sets or updates a custom value for a task/reward.

Payload (backend/events/types/child_override_set.py):

Create a dataclass ChildOverrideSetPayload that inherits from EventPayload with a single field:

  • override (ChildOverride): The override object that was set

TypeScript (frontend/vue-app/src/common/backendEvents.ts):

Create an interface ChildOverrideSetPayload with:

  • override (ChildOverride): The override object that was set

2. child_override_deleted

Emitted When: An override is deleted (manual reset, unassignment, or cascade).

Payload (backend/events/types/child_override_deleted.py):

Create a dataclass ChildOverrideDeletedPayload that inherits from EventPayload with three fields:

  • child_id (str): ID of the child
  • entity_id (str): ID of the entity
  • entity_type (str): Type of entity ('task' or 'reward')

TypeScript (frontend/vue-app/src/common/backendEvents.ts):

Create an interface ChildOverrideDeletedPayload with:

  • child_id (string): ID of the child
  • entity_id (string): ID of the entity
  • entity_type (string): Type of entity

API Design

1. PUT /child/<child_id>/override

Purpose: Set or update a custom value for a task/reward.

Auth: User must own the child.

Request Body:

JSON object with three required fields:

  • entity_id (string): UUID of the task or reward
  • entity_type (string): Either "task" or "reward"
  • custom_value (number): Integer between 0 and 10000

Validation:

  • entity_type must be "task" or "reward"
  • custom_value must be 0-10000
  • Entity must be assigned to child
  • Child must exist and belong to user

Response:

JSON object with a single key override containing the complete ChildOverride object with all fields (id, child_id, entity_id, entity_type, custom_value, created_at, updated_at in ISO format).

Errors:

  • 404: Child not found or not owned
  • 404: Entity not assigned to child
  • 400: Invalid entity_type
  • 400: custom_value out of range

SSE: Emits child_override_set to user.

2. GET /child/<child_id>/overrides

Purpose: Get all overrides for a child.

Auth: User must own the child.

Response:

JSON object with a single key overrides containing an array of ChildOverride objects. Each object includes all standard fields (id, child_id, entity_id, entity_type, custom_value, created_at, updated_at).

Errors:

  • 404: Child not found or not owned

3. DELETE /child/<child_id>/override/<entity_id>

Purpose: Delete an override (reset to default).

Auth: User must own the child.

Response:

JSON object with message field set to "Override deleted".

Errors:

  • 404: Child not found or not owned
  • 404: Override not found

SSE: Emits child_override_deleted to user.

Modified Endpoints

Update these existing endpoints to include override information:

  1. GET /child/<child_id>/list-tasks - Include custom_value in task objects if override exists
  2. GET /child/<child_id>/list-rewards - Include custom_value in reward objects if override exists
  3. POST /child/<child_id>/trigger-task - Use custom_value if override exists when awarding points
  4. POST /child/<child_id>/trigger-reward - Use custom_value if override exists when deducting points
  5. PUT /child/<child_id>/set-tasks - Delete overrides for unassigned tasks
  6. PUT /child/<child_id>/set-rewards - Delete overrides for unassigned rewards

Implementation Details

File Structure

Backend:

  • backend/models/child_override.py - ChildOverride model
  • backend/db/child_overrides.py - Database helpers
  • backend/api/child_override_api.py - New API endpoints (PUT, GET, DELETE)
  • backend/events/types/child_override_set.py - SSE event payload
  • backend/events/types/child_override_deleted.py - SSE event payload
  • backend/events/types/event_types.py - Add CHILD_OVERRIDE_SET, CHILD_OVERRIDE_DELETED enums
  • backend/tests/test_child_override_api.py - Unit tests

Frontend:

  • frontend/vue-app/src/common/models.ts - Add ChildOverride interface
  • frontend/vue-app/src/common/api.ts - Add setChildOverride(), getChildOverrides(), deleteChildOverride()
  • frontend/vue-app/src/common/backendEvents.ts - Add event types
  • frontend/vue-app/src/components/OverrideEditModal.vue - New modal component
  • frontend/vue-app/src/components/ScrollingList.vue - Add edit button and ✏️ badge
  • frontend/vue-app/components/__tests__/OverrideEditModal.spec.ts - Component tests

Logging Strategy

Backend: Log override operations to per-user rotating log files (same pattern as tracking).

Log Messages:

  • Override set: child={child_id}, entity={entity_id}, type={entity_type}, value={custom_value}
  • Override deleted: child={child_id}, entity={entity_id}
  • Overrides cascade deleted for child: child_id={child_id}, count={count}
  • Overrides cascade deleted for entity: entity_id={entity_id}, count={count}

Frontend: No additional logging beyond standard error handling.


Acceptance Criteria (Definition of Done)

Data Model

  • ChildOverride Python dataclass created with validation (0-10000 range, entity_type literal)
  • ChildOverride TypeScript interface created (1:1 parity with Python)
  • child_overrides.json TinyDB table created in backend/db/db.py
  • Database helper functions created (insert, get, delete by child, delete by entity)
  • Composite uniqueness constraint enforced (child_id, entity_id)

Backend Implementation

  • PUT /child/<child_id>/override endpoint created with validation
  • GET /child/<child_id>/overrides endpoint created
  • DELETE /child/<child_id>/override/<entity_id> endpoint created
  • GET /child/<child_id>/list-tasks modified to include custom_value when override exists
  • GET /child/<child_id>/list-rewards modified to include custom_value when override exists
  • POST /child/<child_id>/trigger-task modified to use override value
  • POST /child/<child_id>/trigger-reward modified to use override value
  • PUT /child/<child_id>/set-tasks modified to delete overrides for unassigned tasks
  • PUT /child/<child_id>/set-rewards modified to delete overrides for unassigned rewards
  • Cascade delete implemented: deleting child removes all its overrides
  • Cascade delete implemented: deleting task/reward removes all its overrides
  • Authorization checks: user must own child to access overrides
  • Validation: entity must be assigned to child before override can be set

SSE Events

  • child_override_set event type added to event_types.py
  • child_override_deleted event type added to event_types.py
  • ChildOverrideSetPayload class created (Python)
  • ChildOverrideDeletedPayload class created (Python)
  • PUT endpoint emits child_override_set event
  • DELETE endpoint emits child_override_deleted event
  • Frontend TypeScript interfaces for event payloads created

Frontend Implementation

  • OverrideEditModal.vue component created
  • Modal has number input field with 0-10000 validation
  • Modal disables save button on invalid input (empty, negative, >10000)
  • Modal defaults to current override value or entity default
  • Modal calls PUT /child/<id>/override API on save
  • Edit button (34x34px) added to ScrollingList items
  • Edit button only appears after first click (when item is centered)
  • Edit button uses edit.png icon from public folder
  • ✏️ emoji badge displayed next to points/cost when override exists
  • Badge only shows for items with active overrides
  • Second click on item activates entity (not first click)
  • SSE listeners registered for child_override_set and child_override_deleted
  • Real-time UI updates when override events received

Backend Unit Tests

API Tests (backend/tests/test_child_override_api.py)

  • Test PUT creates new override with valid data
  • Test PUT updates existing override
  • Test PUT returns 400 for custom_value < 0
  • Test PUT returns 400 for custom_value > 10000
  • Test PUT returns 400 for invalid entity_type
  • Test PUT returns 404 for non-existent child
  • Test PUT returns 404 for unassigned entity
  • Test PUT returns 403 for child not owned by user
  • Test PUT emits child_override_set event
  • Test GET returns all overrides for child
  • Test GET returns empty array when no overrides
  • Test GET returns 404 for non-existent child
  • Test GET returns 403 for child not owned by user
  • Test DELETE removes override
  • Test DELETE returns 404 when override doesn't exist
  • Test DELETE returns 404 for non-existent child
  • Test DELETE returns 403 for child not owned by user
  • Test DELETE emits child_override_deleted event

Integration Tests

  • Test list-tasks includes custom_value for overridden tasks
  • Test list-tasks shows default points for non-overridden tasks
  • Test list-rewards includes custom_value for overridden rewards
  • Test trigger-task uses custom_value when awarding points
  • Test trigger-task uses default points when no override
  • Test trigger-reward uses custom_value when deducting points
  • Test trigger-reward uses default cost when no override
  • Test set-tasks deletes overrides for unassigned tasks
  • Test set-tasks preserves overrides for still-assigned tasks
  • Test set-rewards deletes overrides for unassigned rewards
  • Test set-rewards preserves overrides for still-assigned rewards

Cascade Delete Tests

  • Test deleting child removes all its overrides
  • Test deleting task removes all overrides for that task
  • Test deleting reward removes all overrides for that reward
  • Test unassigning task from child deletes override
  • Test reassigning task to child resets override (not preserved)

Edge Cases

  • Test custom_value = 0 is allowed
  • Test custom_value = 10000 is allowed
  • Test cannot set override for entity not assigned to child
  • Test cannot set override for non-existent entity
  • Test multiple children can have different overrides for same entity

Frontend Unit Tests

Component Tests (components/__tests__/OverrideEditModal.spec.ts)

  • Test modal renders with default value
  • Test modal renders with existing override value
  • Test save button disabled when input is empty
  • Test save button disabled when value < 0
  • Test save button disabled when value > 10000
  • Test save button enabled when value is 0-10000
  • Test modal calls API with correct parameters on save
  • Test modal emits close event after successful save
  • Test modal shows error message on API failure
  • Test cancel button closes modal without saving

Component Tests (components/__tests__/ScrollingList.spec.ts)

  • Test edit button hidden before first click
  • Test edit button appears after first click (when centered)
  • Test edit button opens OverrideEditModal
  • Test ✏️ badge displayed when override exists
  • Test ✏️ badge hidden when no override exists
  • Test second click activates entity (not first click)
  • Test edit button positioned correctly (34x34px, corner)
  • Test edit button doesn't interfere with text

Integration Tests

  • Test SSE event updates UI when override is set
  • Test SSE event updates UI when override is deleted
  • Test override value displayed in task/reward list
  • Test points calculation uses override when triggering task
  • Test cost calculation uses override when triggering reward

Edge Cases

  • Test 0 points/cost displays correctly
  • Test 10000 points/cost displays correctly
  • Test badge updates immediately after setting override
  • Test badge disappears immediately after deleting override

Logging & Monitoring

  • Override set operations logged to per-user log files
  • Override delete operations logged
  • Cascade delete operations logged with count
  • Log messages include child_id, entity_id, entity_type, custom_value

Documentation

  • API endpoints documented in this spec
  • Data model documented in this spec
  • SSE events documented in this spec
  • UI behavior documented in this spec
  • Edge cases and validation rules documented

Testing Strategy

Unit Test Files

Backend (backend/tests/test_child_override_api.py):

Create six test classes:

  1. TestChildOverrideModel: Test model validation (6 tests)

    • Valid override creation
    • Negative custom_value raises ValueError
    • custom_value > 10000 raises ValueError
    • custom_value = 0 is allowed
    • custom_value = 10000 is allowed
    • Invalid entity_type raises ValueError
  2. TestChildOverrideDB: Test database operations (8 tests)

    • Insert new override
    • Insert updates existing (upsert behavior)
    • Get existing override returns object
    • Get nonexistent override returns None
    • Get all overrides for a child
    • Delete specific override
    • Delete all overrides for a child (returns count)
    • Delete all overrides for an entity (returns count)
  3. TestChildOverrideAPI: Test all three API endpoints (18 tests)

    • PUT creates new override
    • PUT updates existing override
    • PUT returns 400 for negative value
    • PUT returns 400 for value > 10000
    • PUT returns 400 for invalid entity_type
    • PUT returns 404 for nonexistent child
    • PUT returns 404 for unassigned entity
    • PUT returns 403 for child not owned by user
    • PUT emits child_override_set event
    • GET returns all overrides for child
    • GET returns empty array when no overrides
    • GET returns 404 for nonexistent child
    • GET returns 403 for child not owned
    • DELETE removes override successfully
    • DELETE returns 404 when override doesn't exist
    • DELETE returns 404 for nonexistent child
    • DELETE returns 403 for child not owned
    • DELETE emits child_override_deleted event
  4. TestIntegration: Test override integration with existing endpoints (11 tests)

    • list-tasks includes custom_value for overridden tasks
    • list-tasks shows default points for non-overridden tasks
    • list-rewards includes custom_value for overridden rewards
    • trigger-task uses custom_value when awarding points
    • trigger-task uses default points when no override
    • trigger-reward uses custom_value when deducting points
    • trigger-reward uses default cost when no override
    • set-tasks deletes overrides for unassigned tasks
    • set-tasks preserves overrides for still-assigned tasks
    • set-rewards deletes overrides for unassigned rewards
    • set-rewards preserves overrides for still-assigned rewards
  5. TestCascadeDelete: Test cascade deletion behavior (5 tests)

    • Deleting child removes all its overrides
    • Deleting task removes all overrides for that task
    • Deleting reward removes all overrides for that reward
    • Unassigning task deletes override
    • Reassigning task resets override (not preserved)
  6. TestEdgeCases: Test boundary conditions (5 tests)

    • custom_value = 0 is allowed
    • custom_value = 10000 is allowed
    • Cannot set override for unassigned entity
    • Cannot set override for nonexistent entity
    • Multiple children can have different overrides for same entity

Test Fixtures

Create pytest fixtures for common test scenarios:

  • child_with_task: Uses existing child and task fixtures, calls set-tasks endpoint to assign task to child, asserts 200 response, returns child dict
  • child_with_task_override: Builds on child_with_task, calls PUT override endpoint to set custom_value=15 for the task, asserts 200 response, returns child dict
  • Similar fixtures for rewards: child_with_reward, child_with_reward_override
  • child_with_overrides: Child with multiple overrides for testing bulk operations

Assertions

Test assertions should verify three main areas:

  1. API Response Correctness: Check status code (200, 400, 403, 404), verify returned override object has correct values for all fields (custom_value, child_id, entity_id, etc.)

  2. SSE Event Emission: Use mock_sse fixture to assert send_event_for_current_user was called exactly once with the correct EventType (CHILD_OVERRIDE_SET or CHILD_OVERRIDE_DELETED)

  3. Points Calculation: After triggering tasks/rewards, verify the child's points reflect the custom_value (not the default). For example, if default is 10 but override is 15, child.points should increase by 15.


Future Considerations

  1. Bulk Override Management: Add endpoint to set/get/delete multiple overrides at once for performance.
  2. Override History: Track changes to override values over time for analytics.
  3. Copy Overrides: Allow copying overrides from one child to another.
  4. Override Templates: Save common override patterns as reusable templates.
  5. Percentage-Based Overrides: Allow setting overrides as percentage of default (e.g., "150% of default").
  6. Override Expiration: Add optional expiration dates for temporary adjustments.
  7. Undo Override: Add "Restore Default" button in UI that deletes override with one click.
  8. Admin Dashboard: Show overview of all overrides across all children for analysis.