feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s

- 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.
This commit is contained in:
2026-02-10 20:21:05 -05:00
parent 3dee8b80a2
commit 401c21ad82
45 changed files with 4353 additions and 441 deletions

View File

@@ -1,318 +0,0 @@
# Feature: Account Deletion Scheduler
## Overview
**Goal:** Implement a scheduler in the backend that will delete accounts that are marked for deletion after a period of time.
**User Story:**
As an administrator, I want accounts that are marked for deletion to be deleted around X amount of hours after they were marked. I want the time to be adjustable.
---
## Configuration
### Environment Variables
- `ACCOUNT_DELETION_THRESHOLD_HOURS`: Hours to wait before deleting marked accounts (default: 720 hours / 30 days)
- **Minimum:** 24 hours (enforced for safety)
- **Maximum:** 720 hours (30 days)
- Configurable via environment variable with validation on startup
### Scheduler Settings
- **Check Interval:** Every 1 hour
- **Implementation:** APScheduler (BackgroundScheduler)
- **Restart Handling:** On app restart, scheduler checks for users with `deletion_in_progress = True` and retries them
- **Retry Logic:** Maximum 3 attempts per user; tracked via `deletion_attempted_at` timestamp
---
## Data Model Changes
### User Model (`backend/models/user.py`)
Add two new fields to the `User` dataclass:
- `deletion_in_progress: bool` - Default `False`. Set to `True` when deletion is actively running
- `deletion_attempted_at: datetime | None` - Default `None`. Timestamp of last deletion attempt
**Serialization:**
- Both fields must be included in `to_dict()` and `from_dict()` methods
---
## Deletion Process & Order
When a user is due for deletion (current time >= `marked_for_deletion_at` + threshold), the scheduler performs deletion in this order:
1. **Set Flag:** `deletion_in_progress = True` (prevents concurrent deletion)
2. **Pending Rewards:** Remove all pending rewards for user's children
3. **Children:** Remove all children belonging to the user
4. **Tasks:** Remove all user-created tasks (where `user_id` matches)
5. **Rewards:** Remove all user-created rewards (where `user_id` matches)
6. **Images (Database):** Remove user's uploaded images from `image_db`
7. **Images (Filesystem):** Delete `data/images/[user_id]` directory and all contents
8. **User Record:** Remove the user from `users_db`
9. **Clear Flag:** `deletion_in_progress = False` (only if deletion failed; otherwise user is deleted)
10. **Update Timestamp:** Set `deletion_attempted_at` to current time (if deletion failed)
### Error Handling
- If any step fails, log the error and continue to next step
- If deletion fails completely, update `deletion_attempted_at` and set `deletion_in_progress = False`
- If a user has 3 failed attempts, log a critical error but continue processing other users
- Missing directories or empty tables are not considered errors
---
## Admin API Endpoints
### New Blueprint: `backend/api/admin_api.py`
All endpoints require JWT authentication and admin privileges.
**Note:** Endpoint paths below are as defined in Flask (without `/api` prefix). Frontend accesses them via nginx proxy at `/api/admin/*`.
#### `GET /admin/deletion-queue`
Returns list of users pending deletion.
**Response:** JSON with `count` and `users` array containing user objects with fields: `id`, `email`, `marked_for_deletion_at`, `deletion_due_at`, `deletion_in_progress`, `deletion_attempted_at`
#### `GET /admin/deletion-threshold`
Returns current deletion threshold configuration.
**Response:** JSON with `threshold_hours`, `threshold_min`, and `threshold_max` fields
#### `PUT /admin/deletion-threshold`
Updates deletion threshold (requires admin auth).
**Request:** JSON with `threshold_hours` field
**Response:** JSON with `message` and updated `threshold_hours`
**Validation:**
- Must be between 24 and 720 hours
- Returns 400 error if out of range
#### `POST /admin/deletion-queue/trigger`
Manually triggers the deletion scheduler (processes entire queue immediately).
**Response:** JSON with `message`, `processed`, `deleted`, and `failed` counts
---
## SSE Event
### New Event Type: `USER_DELETED`
**File:** `backend/events/types/user_deleted.py`
**Payload fields:**
- `user_id: str` - ID of deleted user
- `email: str` - Email of deleted user
- `deleted_at: str` - ISO format timestamp of deletion
**Broadcasting:**
- Event is sent only to **admin users** (not broadcast to all users)
- Triggered immediately after successful user deletion
- Frontend admin clients can listen to this event to update UI
---
## Implementation Details
### File Structure
- `backend/config/deletion_config.py` - Configuration with env variable
- `backend/utils/account_deletion_scheduler.py` - Scheduler logic
- `backend/api/admin_api.py` - New admin endpoints
- `backend/events/types/user_deleted.py` - New SSE event
### Scheduler Startup
In `backend/main.py`, import and call `start_deletion_scheduler()` after Flask app setup
### Logging Strategy
**Configuration:**
- Use dedicated logger: `account_deletion_scheduler`
- Log to both stdout (for Docker/dev) and rotating file (for persistence)
- File: `logs/account_deletion.log`
- Rotation: 10MB max file size, keep 5 backups
- Format: `%(asctime)s - %(name)s - %(levelname)s - %(message)s`
**Log Levels:**
- **INFO:** Each deletion step (e.g., "Deleted 5 children for user {user_id}")
- **INFO:** Summary after each run (e.g., "Deletion scheduler run: 3 users processed, 2 deleted, 1 failed")
- **ERROR:** Individual step failures (e.g., "Failed to delete images for user {user_id}: {error}")
- **CRITICAL:** User with 3+ failed attempts (e.g., "User {user_id} has failed deletion 3 times")
- **WARNING:** Threshold set below 168 hours (7 days)
---
## Acceptance Criteria (Definition of Done)
### Data Model
- [x] Add `deletion_in_progress` field to User model
- [x] Add `deletion_attempted_at` field to User model
- [x] Update `to_dict()` and `from_dict()` methods for serialization
- [x] Update TypeScript User interface in frontend
### Configuration
- [x] Create `backend/config/deletion_config.py` with `ACCOUNT_DELETION_THRESHOLD_HOURS`
- [x] Add environment variable support with default (720 hours)
- [x] Enforce minimum threshold of 24 hours
- [x] Enforce maximum threshold of 720 hours
- [x] Log warning if threshold is less than 168 hours
### Backend Implementation
- [x] Create `backend/utils/account_deletion_scheduler.py`
- [x] Implement APScheduler with 1-hour check interval
- [x] Implement deletion logic in correct order (pending_rewards → children → tasks → rewards → images → directory → user)
- [x] Add comprehensive error handling (log and continue)
- [x] Add restart handling (check `deletion_in_progress` flag on startup)
- [x] Add retry logic (max 3 attempts per user)
- [x] Integrate scheduler into `backend/main.py` startup
### Admin API
- [x] Create `backend/api/admin_api.py` blueprint
- [x] Implement `GET /admin/deletion-queue` endpoint
- [x] Implement `GET /admin/deletion-threshold` endpoint
- [x] Implement `PUT /admin/deletion-threshold` endpoint
- [x] Implement `POST /admin/deletion-queue/trigger` endpoint
- [x] Add JWT authentication checks for all admin endpoints
- [x] Add admin role validation
### SSE Event
- [x] Create `backend/events/types/user_deleted.py`
- [x] Add `USER_DELETED` to `event_types.py`
- [x] Implement admin-only event broadcasting
- [x] Trigger event after successful deletion
### Backend Unit Tests
#### Configuration Tests
- [x] Test default threshold value (720 hours)
- [x] Test environment variable override
- [x] Test minimum threshold enforcement (24 hours)
- [x] Test maximum threshold enforcement (720 hours)
- [x] Test invalid threshold values (negative, non-numeric)
#### Scheduler Tests
- [x] Test scheduler identifies users ready for deletion (past threshold)
- [x] Test scheduler ignores users not yet due for deletion
- [x] Test scheduler handles empty database
- [x] Test scheduler runs at correct interval (1 hour)
- [x] Test scheduler handles restart with `deletion_in_progress = True`
- [x] Test scheduler respects retry limit (max 3 attempts)
#### Deletion Process Tests
- [x] Test deletion removes pending_rewards for user's children
- [x] Test deletion removes children for user
- [x] Test deletion removes user's tasks (not system tasks)
- [x] Test deletion removes user's rewards (not system rewards)
- [x] Test deletion removes user's images from database
- [x] Test deletion removes user directory from filesystem
- [x] Test deletion removes user record from database
- [x] Test deletion handles missing directory gracefully
- [x] Test deletion order is correct (children before user, etc.)
- [x] Test `deletion_in_progress` flag is set during deletion
- [x] Test `deletion_attempted_at` is updated on failure
#### Edge Cases
- [x] Test deletion with user who has no children
- [x] Test deletion with user who has no custom tasks/rewards
- [x] Test deletion with user who has no uploaded images
- [x] Test partial deletion failure (continue with other users)
- [x] Test concurrent deletion attempts (flag prevents double-deletion)
- [x] Test user with exactly 3 failed attempts (logs critical, no retry)
#### Admin API Tests
- [x] Test `GET /admin/deletion-queue` returns correct users
- [x] Test `GET /admin/deletion-queue` requires authentication
- [x] Test `GET /admin/deletion-threshold` returns current threshold
- [x] Test `PUT /admin/deletion-threshold` updates threshold
- [x] Test `PUT /admin/deletion-threshold` validates min/max
- [x] Test `PUT /admin/deletion-threshold` requires admin role
- [x] Test `POST /admin/deletion-queue/trigger` triggers scheduler
- [x] Test `POST /admin/deletion-queue/trigger` returns summary
#### Integration Tests
- [x] Test full deletion flow from marking to deletion
- [x] Test multiple users deleted in same scheduler run
- [x] Test deletion with restart midway (recovery)
### Logging & Monitoring
- [x] Configure dedicated scheduler logger with rotating file handler
- [x] Create `logs/` directory for log files
- [x] Log each deletion step with INFO level
- [x] Log summary after each scheduler run (users processed, deleted, failed)
- [x] Log errors with user ID for debugging
- [x] Log critical error for users with 3+ failed attempts
- [x] Log warning if threshold is set below 168 hours
### Documentation
- [x] Create `README.md` at project root
- [x] Document scheduler feature and behavior
- [x] Document environment variable `ACCOUNT_DELETION_THRESHOLD_HOURS`
- [x] Document deletion process and order
- [x] Document admin API endpoints
- [x] Document restart/retry behavior
---
## Testing Strategy
All tests should use `DB_ENV=test` and operate on test databases in `backend/test_data/`.
### Unit Test Files
- `backend/tests/test_deletion_config.py` - Configuration validation
- `backend/tests/test_deletion_scheduler.py` - Scheduler logic
- `backend/tests/test_admin_api.py` - Admin endpoints
### Test Fixtures
- Create users with various `marked_for_deletion_at` timestamps
- Create users with children, tasks, rewards, images
- Create users with `deletion_in_progress = True` (for restart tests)
### Assertions
- Database records are removed in correct order
- Filesystem directories are deleted
- Flags and timestamps are updated correctly
- Error handling works (log and continue)
- Admin API responses match expected format
---
## Future Considerations
- Archive deleted accounts instead of hard deletion
- Email notification to admin when deletion completes
- Configurable retry count (currently hardcoded to 3)
- Soft delete with recovery option (within grace period)

View File

@@ -0,0 +1,87 @@
# Feature: Hash passwords in database
## Overview
**Goal:** Currently passwords for users are stored in the database as plain text. They need to be hashed using a secure algorithm to prevent exposure in case of a data breach.
**User Story:**
As a user, when I create an account with a password, the password needs to be hashed in the database.
As an admin, I would like a script that will convert the current user database passwords into a hash.
---
## Data Model Changes
### Backend Model (`backend/models/user.py`)
No changes required to the `User` dataclass fields. Passwords will remain as strings, but they will now be hashed values instead of plain text.
### Frontend Model (`frontend/vue-app/src/common/models.ts`)
No changes required. The `User` interface does not expose passwords.
---
## Backend Implementation
### Password Hashing
- Use `werkzeug.security.generate_password_hash()` with default settings (PBKDF2 with SHA256, salt, and iterations) for hashing new passwords.
- Use `werkzeug.security.check_password_hash()` for verification during login and password reset.
- Update the following endpoints to hash passwords on input and verify hashes on output:
- `POST /signup` (hash password before storing; existing length/complexity checks apply).
- `POST /login` (verify hash against input).
- `POST /reset-password` (hash new password before storing; existing length/complexity checks apply).
### Migration Script (`backend/scripts/hash_passwords.py`)
Create a new script to hash existing plain text passwords in the database:
- Read all users from `users_db`.
- For each user, check if the password is already hashed (starts with `scrypt:` or `$pbkdf2-sha256$`); if so, skip.
- For plain text passwords, hash using `generate_password_hash()`.
- Update the user record in the database.
- Log the number of users updated.
- Run this script once after deployment to migrate existing data.
**Usage:** `python backend/scripts/hash_passwords.py`
**Security Notes:**
- The script should only be run in a secure environment (e.g., admin access).
- After migration, verify a few users can log in.
- Delete or secure the script post-migration to avoid reuse.
### Error Handling
No new error codes needed. Existing authentication errors (e.g., invalid credentials) remain unchanged.
---
### Backend Tests (`backend/tests/test_auth_api.py`)
- [x] Test signup with password hashing: Verify stored password is hashed (starts with `scrypt:`).
- [x] Test login with correct password: Succeeds.
- [x] Test login with incorrect password: Fails with appropriate error.
- [x] Test password reset: New password is hashed.
- [x] Test migration script: Hashes existing plain text passwords without data loss; skips already-hashed passwords.
---
## Future Considerations
- Monitor for deprecated hashing algorithms and plan upgrades (e.g., to Argon2 if needed).
- Implement password strength requirements on signup/reset if not already present.
- Consider rate limiting on login attempts to prevent brute-force attacks.
---
## Acceptance Criteria (Definition of Done)
### Backend
- [x] Update `/signup` to hash passwords using `werkzeug.security.generate_password_hash()`.
- [x] Update `/login` to verify passwords using `werkzeug.security.check_password_hash()`.
- [x] Update `/reset-password` to hash new passwords.
- [x] Create `backend/scripts/hash_passwords.py` script for migrating existing plain text passwords.
- [x] All backend tests pass, including new hashing tests.

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,519 @@
# 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](feat-dynamic-points-before.png)
- After first click: [feat-dynamic-points-after.png](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
- [x] `ChildOverride` Python dataclass created with validation (0-10000 range, entity_type literal)
- [x] `ChildOverride` TypeScript interface created (1:1 parity with Python)
- [x] `child_overrides.json` TinyDB table created in `backend/db/db.py`
- [x] Database helper functions created (insert, get, delete by child, delete by entity)
- [x] Composite uniqueness constraint enforced (child_id, entity_id)
### Backend Implementation
- [x] PUT `/child/<child_id>/override` endpoint created with validation
- [x] GET `/child/<child_id>/overrides` endpoint created
- [x] DELETE `/child/<child_id>/override/<entity_id>` endpoint created
- [x] GET `/child/<child_id>/list-tasks` modified to include `custom_value` when override exists
- [x] GET `/child/<child_id>/list-rewards` modified to include `custom_value` when override exists
- [x] POST `/child/<child_id>/trigger-task` modified to use override value
- [x] POST `/child/<child_id>/trigger-reward` modified to use override value
- [x] PUT `/child/<child_id>/set-tasks` modified to delete overrides for unassigned tasks
- [x] PUT `/child/<child_id>/set-rewards` modified to delete overrides for unassigned rewards
- [x] Cascade delete implemented: deleting child removes all its overrides
- [x] Cascade delete implemented: deleting task/reward removes all its overrides
- [x] Authorization checks: user must own child to access overrides
- [x] Validation: entity must be assigned to child before override can be set
### SSE Events
- [x] `child_override_set` event type added to event_types.py
- [x] `child_override_deleted` event type added to event_types.py
- [x] `ChildOverrideSetPayload` class created (Python)
- [x] `ChildOverrideDeletedPayload` class created (Python)
- [x] PUT endpoint emits `child_override_set` event
- [x] DELETE endpoint emits `child_override_deleted` event
- [x] Frontend TypeScript interfaces for event payloads created
### Frontend Implementation
- [x] `OverrideEditModal.vue` component created
- [x] Modal has number input field with 0-10000 validation
- [x] Modal disables save button on invalid input (empty, negative, >10000)
- [x] Modal defaults to current override value or entity default
- [x] Modal calls PUT `/child/<id>/override` API on save
- [x] Edit button (34x34px) added to ScrollingList items
- [x] Edit button only appears after first click (when item is centered)
- [x] Edit button uses `edit.png` icon from public folder
- [x] ✏️ emoji badge displayed next to points/cost when override exists
- [x] Badge only shows for items with active overrides
- [x] Second click on item activates entity (not first click)
- [x] SSE listeners registered for `child_override_set` and `child_override_deleted`
- [x] Real-time UI updates when override events received
### Backend Unit Tests
#### API Tests (`backend/tests/test_child_override_api.py`)
- [x] Test PUT creates new override with valid data
- [x] Test PUT updates existing override
- [x] Test PUT returns 400 for custom_value < 0
- [x] Test PUT returns 400 for custom_value > 10000
- [x] 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
- [x] 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
- [x] 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
- [x] Test deleting child removes all its overrides
- [x] Test deleting task removes all overrides for that task
- [x] Test deleting reward removes all overrides for that reward
- [x] Test unassigning task from child deletes override
- [x] Test reassigning task to child resets override (not preserved)
#### Edge Cases
- [x] Test custom_value = 0 is allowed
- [x] 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`)
- [x] Test modal renders with default value
- [x] Test modal renders with existing override value
- [x] Test save button disabled when input is empty
- [x] Test save button disabled when value < 0
- [x] Test save button disabled when value > 10000
- [x] Test save button enabled when value is 0-10000
- [x] Test modal calls API with correct parameters on save
- [x] Test modal emits close event after successful save
- [x] Test modal shows error message on API failure
- [x] 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.