feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s
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:
@@ -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)
|
||||
87
.github/specs/active/feat-hashed-passwords.md
vendored
Normal file
87
.github/specs/active/feat-hashed-passwords.md
vendored
Normal 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.
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
BIN
.github/specs/archive/feat-dynamic-points/feat-dynamic-points-edit-modal.png
vendored
Normal file
BIN
.github/specs/archive/feat-dynamic-points/feat-dynamic-points-edit-modal.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
519
.github/specs/archive/feat-dynamic-points/feat-dynamic-points.md
vendored
Normal file
519
.github/specs/archive/feat-dynamic-points/feat-dynamic-points.md
vendored
Normal 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.
|
||||
Reference in New Issue
Block a user