- 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.
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:
- Assignment First: Tasks, penalties, and rewards must be assigned to a child before their points/cost can be customized.
- 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.pngicon). - Modal Customization: Clicking the edit button opens a modal with a number input field allowing values from 0 to 10000.
- Default Values: The field defaults to the last user-set value or the entity's default points/cost if never customized.
- Visual Indicator: Items with custom values show a ✏️ emoji badge next to the points/cost number.
- 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.jsontable (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 toentity_id(str): ID of the task/penalty/reward being customizedentity_type(Literal['task', 'reward']): Type of entitycustom_value(int): Custom points or cost value
Validation requirements:
custom_valuemust be between 0 and 10000 (inclusive)entity_typemust 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
EntityTypeas 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_valuewhich 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 combinationget_overrides_for_child(child_id): Return List[ChildOverride] for all overrides belonging to a childdelete_override(child_id, entity_id): Delete specific override, return bool indicating successdelete_overrides_for_child(child_id): Delete all overrides for a child, return count deleteddelete_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 childentity_id(str): ID of the entityentity_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 childentity_id(string): ID of the entityentity_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 rewardentity_type(string): Either "task" or "reward"custom_value(number): Integer between 0 and 10000
Validation:
entity_typemust be "task" or "reward"custom_valuemust 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:
- GET
/child/<child_id>/list-tasks- Includecustom_valuein task objects if override exists - GET
/child/<child_id>/list-rewards- Includecustom_valuein reward objects if override exists - POST
/child/<child_id>/trigger-task- Usecustom_valueif override exists when awarding points - POST
/child/<child_id>/trigger-reward- Usecustom_valueif override exists when deducting points - PUT
/child/<child_id>/set-tasks- Delete overrides for unassigned tasks - PUT
/child/<child_id>/set-rewards- Delete overrides for unassigned rewards
Implementation Details
File Structure
Backend:
backend/models/child_override.py- ChildOverride modelbackend/db/child_overrides.py- Database helpersbackend/api/child_override_api.py- New API endpoints (PUT, GET, DELETE)backend/events/types/child_override_set.py- SSE event payloadbackend/events/types/child_override_deleted.py- SSE event payloadbackend/events/types/event_types.py- Add CHILD_OVERRIDE_SET, CHILD_OVERRIDE_DELETED enumsbackend/tests/test_child_override_api.py- Unit tests
Frontend:
frontend/vue-app/src/common/models.ts- Add ChildOverride interfacefrontend/vue-app/src/common/api.ts- Add setChildOverride(), getChildOverrides(), deleteChildOverride()frontend/vue-app/src/common/backendEvents.ts- Add event typesfrontend/vue-app/src/components/OverrideEditModal.vue- New modal componentfrontend/vue-app/src/components/ScrollingList.vue- Add edit button and ✏️ badgefrontend/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
ChildOverridePython dataclass created with validation (0-10000 range, entity_type literal)ChildOverrideTypeScript interface created (1:1 parity with Python)child_overrides.jsonTinyDB table created inbackend/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>/overrideendpoint created with validation - GET
/child/<child_id>/overridesendpoint created - DELETE
/child/<child_id>/override/<entity_id>endpoint created - GET
/child/<child_id>/list-tasksmodified to includecustom_valuewhen override exists - GET
/child/<child_id>/list-rewardsmodified to includecustom_valuewhen override exists - POST
/child/<child_id>/trigger-taskmodified to use override value - POST
/child/<child_id>/trigger-rewardmodified to use override value - PUT
/child/<child_id>/set-tasksmodified to delete overrides for unassigned tasks - PUT
/child/<child_id>/set-rewardsmodified 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_setevent type added to event_types.pychild_override_deletedevent type added to event_types.pyChildOverrideSetPayloadclass created (Python)ChildOverrideDeletedPayloadclass created (Python)- PUT endpoint emits
child_override_setevent - DELETE endpoint emits
child_override_deletedevent - Frontend TypeScript interfaces for event payloads created
Frontend Implementation
OverrideEditModal.vuecomponent 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>/overrideAPI on save - Edit button (34x34px) added to ScrollingList items
- Edit button only appears after first click (when item is centered)
- Edit button uses
edit.pngicon 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_setandchild_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:
-
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
-
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)
-
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
-
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
-
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)
-
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 existingchildandtaskfixtures, calls set-tasks endpoint to assign task to child, asserts 200 response, returns child dictchild_with_task_override: Builds onchild_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:
-
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.)
-
SSE Event Emission: Use mock_sse fixture to assert
send_event_for_current_userwas called exactly once with the correct EventType (CHILD_OVERRIDE_SET or CHILD_OVERRIDE_DELETED) -
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
- Bulk Override Management: Add endpoint to set/get/delete multiple overrides at once for performance.
- Override History: Track changes to override values over time for analytics.
- Copy Overrides: Allow copying overrides from one child to another.
- Override Templates: Save common override patterns as reusable templates.
- Percentage-Based Overrides: Allow setting overrides as percentage of default (e.g., "150% of default").
- Override Expiration: Add optional expiration dates for temporary adjustments.
- Undo Override: Add "Restore Default" button in UI that deletes override with one click.
- Admin Dashboard: Show overview of all overrides across all children for analysis.