Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,36 @@ jobs:
fi

$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml pull
$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml up -d mysql

DB_ROOT_PASSWORD="$(get_env_value MYSQL_ROOT_PASSWORD)"
DB_NAME="$(get_env_value MYSQL_DATABASE)"

for attempt in $(seq 1 30); do
MYSQL_STATUS="$(sudo docker inspect -f '{{.State.Health.Status}}' ontime-dev-mysql 2>/dev/null || true)"
if [ "$MYSQL_STATUS" = "healthy" ]; then
echo "MySQL container is healthy."
break
fi
echo "Waiting for healthy MySQL status; current status: ${MYSQL_STATUS:-unknown}"
sleep 5
done

MYSQL_STATUS="$(sudo docker inspect -f '{{.State.Health.Status}}' ontime-dev-mysql 2>/dev/null || true)"
[ "$MYSQL_STATUS" = "healthy" ] || fail_deploy "MySQL container did not become healthy."

FAILED_V17_COUNT="$(
sudo docker exec ontime-dev-mysql \
mysql -u root --password="$DB_ROOT_PASSWORD" "$DB_NAME" \
-Nse "SELECT COUNT(*) FROM flyway_schema_history WHERE version = '17' AND success = 0;" 2>/dev/null || echo 0
)"
if [ "$FAILED_V17_COUNT" != "0" ]; then
echo "Repairing failed development Flyway migration V17 before backend startup."
sudo docker exec ontime-dev-mysql \
mysql -u root --password="$DB_ROOT_PASSWORD" "$DB_NAME" \
-e "DROP TABLE IF EXISTS user_refresh_token; DELETE FROM flyway_schema_history WHERE version = '17' AND success = 0;"
fi

$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml up -d --remove-orphans

HEALTHY=false
Expand Down
2 changes: 2 additions & 0 deletions docs/account-deletion-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Account deletion hard-deletes the user from OnTime. The request can optionally i

For Google and Apple social accounts, the backend first tries to revoke the social login token, then deletes the local OnTime account. If provider token revocation fails, the backend logs a warning and still deletes the local OnTime account.

Account deletion also deletes account-scoped settings and preferences, including the analytics preference stored in `user_analytics_preference`. Future user-linked Product Usage Events stop after deletion; historical analytics may be retained only in aggregate or de-identified form under the approved privacy policy and analytics provider configuration.

For release/privacy evidence by data category, see `docs/account-deletion-verification-evidence.md`.

## Authentication
Expand Down
1 change: 1 addition & 0 deletions docs/account-deletion-verification-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ request timestamp, response, owner, and evidence link.
| Access and refresh tokens | `user.access_token`, `user.refresh_token`, `user_device.session_access_token`, `user_device.session_refresh_token` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete and device cascade | Test asserts user and device rows are absent | Backend repo; release env owner TBD |
| Device records and FCM tokens | `user.firebase_token`, `user_device`, `user_device.firebase_token` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete and device cascade | Test asserts user device row is absent | Backend repo; release env owner TBD |
| Alarm settings and alarm status | `user_alarm_setting`, `user_alarm_status` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user/device | Test asserts alarm setting and status rows are absent | Backend repo; release env owner TBD |
| Analytics preference | `user_analytics_preference` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user | Test asserts analytics preference row is absent | Backend repo; release env owner TBD |
| Default preparation settings | `preparation_user` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user | Test asserts preparation user rows are absent | Backend repo; release env owner TBD |
| Schedules | `schedule`, `notification_schedule` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user and schedule | Test asserts schedule and notification schedule rows are absent | Backend repo; release env owner TBD |
| Schedule preparation steps | `preparation_schedule` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from schedule | Test asserts preparation schedule rows are absent | Backend repo; release env owner TBD |
Expand Down
91 changes: 91 additions & 0 deletions docs/analytics-preference-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Analytics Preference API

Issue: #318

## Summary

The analytics preference API stores whether the signed-in account allows optional
Product Usage Events. The preference is account-scoped, not device-scoped.

This API does not define Firebase event names, frontend instrumentation, local
pre-login preference storage, UI copy, marketing analytics, personalization, or
Firebase Remote Config behavior.

## Default And Release Gate

Backend default is controlled by:

```properties
analytics.preference.default-enabled=${ANALYTICS_PREFERENCE_DEFAULT_ENABLED:false}
```

The default remains `false` until the privacy policy and Google Play Data Safety
updates are approved for the Firebase Analytics release. After approval, deploy
owners may set `ANALYTICS_PREFERENCE_DEFAULT_ENABLED=true`.

Rows created before the default is flipped are marked as not user-overridden.
When the service reads a non-overridden row, it may align that row to the current
deploy default. Once a user explicitly updates the preference, the row is marked
as user-overridden and future default flips do not change that choice.

## Get Analytics Preference

```http
GET /users/me/analytics-preference
Authorization: Bearer <access token>
```

Successful response:

```json
{
"status": "success",
"code": 200,
"message": "OK",
"data": {
"enabled": false,
"updatedAt": "2026-05-26T12:00:00Z"
}
}
```

## Update Analytics Preference

```http
PUT /users/me/analytics-preference
Authorization: Bearer <access token>
Content-Type: application/json

{
"enabled": true
}
```

Successful response:

```json
{
"status": "success",
"code": 200,
"message": "OK",
"data": {
"enabled": true,
"updatedAt": "2026-05-26T12:00:05Z"
}
}
```

`enabled` is required and must be a JSON boolean. Missing, null, non-boolean, or
unknown fields are rejected with the existing validation-style `400` response.

## Persistence And Deletion

The preference is stored in `user_analytics_preference` with a unique foreign key
to `user(user_id)`, `enabled`, `updated_at`, and the internal
`user_overridden` flag.

On account deletion, the local analytics preference row is deleted by foreign-key
cascade. Future user-linked Product Usage Events stop when the account
preference is disabled or the account is deleted. Historical analytics may be
retained only in aggregate or de-identified form, subject to the approved privacy
policy and Firebase/analytics project configuration.
248 changes: 248 additions & 0 deletions docs/preparation-templates-frontend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# Preparation Templates Frontend Contract

## Concepts
Schedules now have one preparation source:

- `DEFAULT`: uses the user's fixed default preparation from `GET /users/preparations`.
- `TEMPLATE`: uses a named preparation template from `GET /preparation-templates`.
- `CUSTOM`: uses schedule-specific preparation steps.

Started schedules are frozen. Use `preparationFrozen` from schedule responses; it is `true` when `startedAt` is present. Frozen schedules still show their original `preparationMode`, but their preparation steps come from the schedule snapshot.

## Ordered Preparation Shape
New APIs use ordered steps:

```json
{
"preparationId": "3fa85f64-5717-4562-b3fc-2c963f66afe5",
"preparationName": "Shower",
"preparationTime": 15,
"orderIndex": 0
}
```

Rules:
- Client provides UUIDs for templates and preparation steps.
- `orderIndex` is zero-based and contiguous.
- Arrays may be sent in any order; backend stores and returns by `orderIndex`.
- Each list must contain 1-50 steps.
- Each `preparationTime` must be 1-1440 minutes.
- Total preparation time per list must be at most 1440 minutes.
- Step names are trimmed; duplicate step names are allowed.
- Step IDs must not be reused across other templates, other schedules, or the fixed default. Reusing the same step ID within the same resource update is allowed.

## Named Template APIs
The fixed default preparation is not included in these endpoints.

### List Active Templates
`GET /preparation-templates`

Returns active named templates with full steps. Deleted templates are excluded.

```json
{
"status": "success",
"data": [
{
"templateId": "11111111-1111-1111-1111-111111111111",
"templateName": "Work",
"createdAt": "2026-05-14T02:10:00Z",
"updatedAt": "2026-05-14T02:10:00Z",
"deletedAt": null,
"preparations": []
}
]
}
```

### Get Template Detail
`GET /preparation-templates/{templateId}`

Works for active and soft-deleted templates owned by the user. Detail includes `deletedAt`.

### Create Template
`POST /preparation-templates`

```json
{
"templateId": "11111111-1111-1111-1111-111111111111",
"templateName": "Work",
"preparations": [
{
"preparationId": "22222222-2222-2222-2222-222222222222",
"preparationName": "Pack laptop",
"preparationTime": 5,
"orderIndex": 0
}
]
}
```

Active template names are unique per user after trimming and case-insensitive normalization. Deleted template names can be reused, but deleted template IDs cannot.

### Update Template
`PUT /preparation-templates/{templateId}`

Full replace of name and steps. Deleted templates cannot be updated.

### Delete Template
`DELETE /preparation-templates/{templateId}`

Soft-deletes the template. Existing schedules that already use the template keep using it. New schedule create/update cannot select it. Repeated delete succeeds.

## Schedule Create
`POST /schedules`

Preparation source is inferred:

- Omit both `preparationTemplateId` and `customPreparations`: creates `DEFAULT`.
- Send only `preparationTemplateId`: creates `TEMPLATE`.
- Send only `customPreparations`: creates `CUSTOM`.
- Send both: rejected.

Template mode:

```json
{
"scheduleId": "33333333-3333-3333-3333-333333333333",
"placeId": "44444444-4444-4444-4444-444444444444",
"placeName": "Office",
"scheduleName": "Morning meeting",
"moveTime": 20,
"scheduleTime": "2026-06-01T09:30:00",
"scheduleSpareTime": 10,
"scheduleNote": "Bring laptop",
"preparationTemplateId": "11111111-1111-1111-1111-111111111111"
}
```

Custom mode:

```json
{
"scheduleId": "33333333-3333-3333-3333-333333333333",
"placeId": "44444444-4444-4444-4444-444444444444",
"placeName": "Office",
"scheduleName": "Morning meeting",
"moveTime": 20,
"scheduleTime": "2026-06-01T09:30:00",
"customPreparations": [
{
"preparationId": "55555555-5555-5555-5555-555555555555",
"preparationName": "Pack laptop",
"preparationTime": 5,
"orderIndex": 0
}
]
}
```

## Schedule Update
`PUT /schedules/{scheduleId}`

If `preparationMode` is omitted, the current preparation source is preserved. This includes schedules linked to soft-deleted templates.

To change source:

```json
{
"placeId": "44444444-4444-4444-4444-444444444444",
"placeName": "Office",
"scheduleName": "Morning meeting",
"moveTime": 20,
"scheduleTime": "2026-06-01T09:30:00",
"preparationMode": "DEFAULT"
}
```

```json
{
"placeId": "44444444-4444-4444-4444-444444444444",
"placeName": "Office",
"scheduleName": "Morning meeting",
"moveTime": 20,
"scheduleTime": "2026-06-01T09:30:00",
"preparationMode": "TEMPLATE",
"preparationTemplateId": "11111111-1111-1111-1111-111111111111"
}
```

```json
{
"placeId": "44444444-4444-4444-4444-444444444444",
"placeName": "Office",
"scheduleName": "Morning meeting",
"moveTime": 20,
"scheduleTime": "2026-06-01T09:30:00",
"preparationMode": "CUSTOM",
"customPreparations": [
{
"preparationId": "55555555-5555-5555-5555-555555555555",
"preparationName": "Pack laptop",
"preparationTime": 5,
"orderIndex": 0
}
]
}
```

Mixed mode payloads are rejected:
- `DEFAULT` with template ID or custom list.
- `TEMPLATE` without template ID.
- `TEMPLATE` with custom list.
- `CUSTOM` without full custom list.
- `CUSTOM` with template ID.

Started schedules cannot be edited.

## Schedule Responses
Normal schedule list/detail responses include metadata, not full step lists:

```json
{
"scheduleId": "33333333-3333-3333-3333-333333333333",
"scheduleName": "Morning meeting",
"startedAt": null,
"finishedAt": null,
"preparationMode": "TEMPLATE",
"preparationTemplateId": "11111111-1111-1111-1111-111111111111",
"preparationTemplateName": "Work",
"preparationTemplateDeleted": false,
"preparationFrozen": false
}
```

For default and custom schedules, `preparationTemplateId` and `preparationTemplateName` are `null`.

For schedules linked to a deleted template:

```json
{
"preparationMode": "TEMPLATE",
"preparationTemplateId": "11111111-1111-1111-1111-111111111111",
"preparationTemplateName": "Work",
"preparationTemplateDeleted": true
}
```

## Existing Compatibility Endpoints
These remain linked-list shaped:

- `GET /users/preparations`
- `PUT /users/preparations`
- `GET /schedules/{scheduleId}/preparations`
- `POST /schedules/{scheduleId}/preparations`
- `PUT /schedules/{scheduleId}/preparations`

`POST/PUT /schedules/{scheduleId}/preparations` now means "make this schedule CUSTOM" and clears any template link. The request still uses `nextPreparationId`.

## Alarm Window
`GET /schedules/alarm-window` continues to include full `preparations`, and now also includes the same preparation metadata fields as normal schedule responses.

## Error Codes To Handle
- `PREPARATION_TEMPLATE_NOT_FOUND`: missing or cross-user template.
- `PREPARATION_TEMPLATE_NAME_DUPLICATE`: active template name already exists.
- `PREPARATION_TEMPLATE_LIMIT_EXCEEDED`: user already has 20 active named templates.
- `PREPARATION_TEMPLATE_DELETED`: user tried to select or update an owned deleted template.
- `PREPARATION_STEP_ID_CONFLICT`: provided step ID belongs to another preparation resource.
- `INVALID_INPUT`: malformed mode combination, ordering, list size, duration, or linked-list payload.
Loading
Loading