feat: enhance push notification service worker for chore expirations and update related configurations
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m22s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m22s
This commit is contained in:
6
.github/specs/feat-notify-ending-chore.md
vendored
6
.github/specs/feat-notify-ending-chore.md
vendored
@@ -81,7 +81,7 @@ Import added and `start_chore_expiry_notification_scheduler(app)` called after t
|
|||||||
- [x] `_build_push_payload`: single chore → named title, deep-link fields set
|
- [x] `_build_push_payload`: single chore → named title, deep-link fields set
|
||||||
- [x] `_build_push_payload`: multiple chores → generic title, child_id/entity_id null
|
- [x] `_build_push_payload`: multiple chores → generic title, child_id/entity_id null
|
||||||
- [x] `_build_push_payload`: body groups chore names by child
|
- [x] `_build_push_payload`: body groups chore names by child
|
||||||
- [x] `_build_push_payload`: no approve_token or deny_token in payload
|
- [x] `_build_push_payload`: no approve_token or reject_token in payload
|
||||||
- [x] `send_chore_expiry_notifications_for_user`: calls send_push_to_user with correct payload
|
- [x] `send_chore_expiry_notifications_for_user`: calls send_push_to_user with correct payload
|
||||||
- [x] `send_chore_expiry_notifications_for_user`: no push when no chores expiring
|
- [x] `send_chore_expiry_notifications_for_user`: no push when no chores expiring
|
||||||
- [x] `run_chore_expiry_check`: skips when DB_ENV=e2e
|
- [x] `run_chore_expiry_check`: skips when DB_ENV=e2e
|
||||||
@@ -111,7 +111,7 @@ Import added and `start_chore_expiry_notification_scheduler(app)` called after t
|
|||||||
|
|
||||||
**`frontend/vue-app/public/sw.js`** (modified)
|
**`frontend/vue-app/public/sw.js`** (modified)
|
||||||
|
|
||||||
In the `push` event handler, the `actions` array passed to `showNotification` is now conditionally set: when `payload.type === 'chore_expiring_soon'` the array is empty (no buttons); otherwise Approve and Deny buttons are shown as before. The `notificationclick` handler requires no changes — tapping the notification body already navigates to `/parent/{child_id}?scrollTo={entity_id}` when those fields are present, and falls back to `/parent` when they are null (multi-chore case).
|
In the `push` event handler, the `actions` array passed to `showNotification` is now conditionally set: when `payload.type === 'chore_expiring_soon'` the array is empty (no buttons); otherwise Approve and Reject buttons are shown as before. The `notificationclick` handler requires no changes — tapping the notification body already navigates to `/parent/{child_id}?scrollTo={entity_id}` when those fields are present, and falls back to `/parent` when they are null (multi-chore case).
|
||||||
|
|
||||||
## Frontend Tests
|
## Frontend Tests
|
||||||
|
|
||||||
@@ -143,6 +143,6 @@ No automated tests added. The `sw.js` change is a simple conditional and is cove
|
|||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
- [x] `sw.js` updated — `chore_expiring_soon` notifications show no Approve/Deny action buttons
|
- [x] `sw.js` updated — `chore_expiring_soon` notifications show no Approve/Reject action buttons
|
||||||
- [x] Tapping a single-chore notification navigates to the child's chore page
|
- [x] Tapping a single-chore notification navigates to the child's chore page
|
||||||
- [x] Tapping a multi-chore notification navigates to `/parent`
|
- [x] Tapping a multi-chore notification navigates to `/parent`
|
||||||
|
|||||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -14,7 +14,8 @@
|
|||||||
"REFRESH_TOKEN_EXPIRY_DAYS": "90",
|
"REFRESH_TOKEN_EXPIRY_DAYS": "90",
|
||||||
"DIGEST_TOKEN_SECRET": "dev-digest-token",
|
"DIGEST_TOKEN_SECRET": "dev-digest-token",
|
||||||
"VAPID_PUBLIC_KEY": "BNKkHdq45uLigohSG7c1TwlAo7ETncoRVLQK02LxHgu2P1DgSJD9njRMfbbzUsaTQGllvLBz7An1WiWsNYQhvKE",
|
"VAPID_PUBLIC_KEY": "BNKkHdq45uLigohSG7c1TwlAo7ETncoRVLQK02LxHgu2P1DgSJD9njRMfbbzUsaTQGllvLBz7An1WiWsNYQhvKE",
|
||||||
"VAPID_PRIVATE_KEY": "jNiZJT0UO4H861KmnCt874Fg6p5jDAyYKS4V2MZf8bQ"
|
"VAPID_PRIVATE_KEY": "jNiZJT0UO4H861KmnCt874Fg6p5jDAyYKS4V2MZf8bQ",
|
||||||
|
"FRONTEND_URL": "https://macbook:5173"
|
||||||
},
|
},
|
||||||
"args": [
|
"args": [
|
||||||
"run",
|
"run",
|
||||||
|
|||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -37,6 +37,9 @@ __screenshots__/
|
|||||||
|
|
||||||
*.old
|
*.old
|
||||||
|
|
||||||
|
# Local dev TLS certs (machine-specific, generated by mkcert)
|
||||||
|
*.pem
|
||||||
|
|
||||||
# Playwright
|
# Playwright
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCh96Qoz16kpDde
|
|
||||||
puFkARSFp9PDFs8kUBGCJ0DcBDqJIUuLJj7RT5vxfUj3O7uUUjTtVAVCMJ84/eq/
|
|
||||||
IQnyhch1B2Fe5bas51kEgwybzxxEN1bcj3yokbd4aOCpxkpM382TKMPuGH1BXUO8
|
|
||||||
ZqqikbvKySJ9glboPImLRA4UvH1cx9Db0kQE9scBl98qqE28exlBqMBsIOjh+mJ0
|
|
||||||
rWt3JO+A50OjOSuxO7hdjINlJg5X+jmI1wF3GWE2i9GrCJXh9JVYixnjvsFMIKJo
|
|
||||||
Zu9wAZCmtqHvicvwdzDxdDAU8GfACOg4w4hlMOSfIU4pAONxW479Gf5pSFVTw7Zu
|
|
||||||
RNrTDsd9AgMBAAECggEAGICASL0wLdt61dqMg8anDl4Yaq2nafCj6WrbRL1uBoMv
|
|
||||||
LK59N8hhiKuBj4cthg9WmuWIQx5cY/CDo+TRXqsu60dRy1uYYjlATe6uSF7RQZ+W
|
|
||||||
iBi7zLt4hCJnhD9vS4hazs2OsFTrk+kSP2zPmPbPcCqzyUVftNO9ogAKWkg2dcO1
|
|
||||||
9KjDnR0pSn17rwj064mfVNoLYiN11nwQ21zyJFLYGM5mXYYW9b+EI9ZnWWyz6Lvo
|
|
||||||
+qrYhZh2hR8ul8rpURc0flqFvORaO4FhaQeX5+r5FZFQkRWQYbRfBbAJnIrFVa+N
|
|
||||||
8O002E0FLHWI0S7YPsbnBqE+eHRM9fzz1q0p4YrjEQKBgQDDz9n2BLwwQCreXglG
|
|
||||||
V2T1SBdi1+MDTMUCHEcak3Gk4ZRIU4r2ofoTdYagEaYCMBGL7/noYECpEfkzF4B5
|
|
||||||
u1nXUTtFODq0m1Uc18iL8WMJy8AkoT+LZEmF0y5WDI/fLzzxuQKXpTDyeUwTYf/W
|
|
||||||
9xHHi4gckc3wcf1tKtNSswCcMQKBgQDTwJxki8/2OEtIRcPlhhXNQRpSnp6cjgoB
|
|
||||||
xQ4J83idR3/HBLWMI9snx47SE1AxGFY4UHIQNvMN3icKt5C7QyfrMfZCpUcRryv6
|
|
||||||
1dBFVbvxm0Hvqj/pWsgtjbq+dbiOLtH52zc9nsPAF++FhlYtZz9fJmwsgxsnab1O
|
|
||||||
B9dUyXMpDQKBgDj3LRfPhNgcstwCS3x1TF+3W2ZcHCUHnoDgrSbkIjmvjq4D7/eU
|
|
||||||
Y+ZpWIMU31Dfnxsw82lRJz6IhhEBE1VW1eo4LaATnbCRSA+eDy/3R7K/3eRKLOxm
|
|
||||||
fqU6LM7H1Ms/OOGxyzlGy5ifBSzWY9GsCzYcN7roCBudbfbmcJgsj07hAoGAd2p1
|
|
||||||
CCLsub9PfUeSzTrLyr//Nz6q1kEoFY1qeGQszg3HWpYmSAzkh897lK89lyJRZVrA
|
|
||||||
qLJEabqxq9KPtXuO5I19gmIw7SErnT69QIyz+/IBwkXx2wjOQRpfiQ9ccBqpYc2l
|
|
||||||
noONgyQ8eMGkkeBbFa7WbFfXlWeFUZ8MaY1d+3UCgYBguVXJ9JTvMxeXx5cjnfMI
|
|
||||||
eVRhr2BbARTdCJXKUpo4rUUqLrCCSB6BrT4H+DYtRVKO/ZIaXARQIN1ACo03C78o
|
|
||||||
U+Rg+mGTyvGh8pVH63zZE9tsohtT+/bSraYm/dLKUmaK3Z9wG75wmOpNLlAEiUqW
|
|
||||||
mzkW/UN0WWzg5gyODVbQKQ==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEZDCCAsygAwIBAgIQIM51sKaVetu2bhpqghM+2TANBgkqhkiG9w0BAQsFADCB
|
|
||||||
kzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTQwMgYDVQQLDCtyeWFu
|
|
||||||
QG1hY2Jvb2sudGFpbGExNDZiMy50cy5uZXQgKFJ5YW4gS2VnZWwpMTswOQYDVQQD
|
|
||||||
DDJta2NlcnQgcnlhbkBtYWNib29rLnRhaWxhMTQ2YjMudHMubmV0IChSeWFuIEtl
|
|
||||||
Z2VsKTAeFw0yNjA0MTYyMjEwMTNaFw0yODA3MTYyMjEwMTNaMFMxJzAlBgNVBAoT
|
|
||||||
Hm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEoMCYGA1UECwwfcnlhbkBy
|
|
||||||
eWFucy1haXIubGFuIChSeWFuIEtlZ2VsKTCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
|
||||||
ADCCAQoCggEBAKH3pCjPXqSkN16m4WQBFIWn08MWzyRQEYInQNwEOokhS4smPtFP
|
|
||||||
m/F9SPc7u5RSNO1UBUIwnzj96r8hCfKFyHUHYV7ltqznWQSDDJvPHEQ3VtyPfKiR
|
|
||||||
t3ho4KnGSkzfzZMow+4YfUFdQ7xmqqKRu8rJIn2CVug8iYtEDhS8fVzH0NvSRAT2
|
|
||||||
xwGX3yqoTbx7GUGowGwg6OH6YnSta3ck74DnQ6M5K7E7uF2Mg2UmDlf6OYjXAXcZ
|
|
||||||
YTaL0asIleH0lViLGeO+wUwgomhm73ABkKa2oe+Jy/B3MPF0MBTwZ8AI6DjDiGUw
|
|
||||||
5J8hTikA43Fbjv0Z/mlIVVPDtm5E2tMOx30CAwEAAaNzMHEwDgYDVR0PAQH/BAQD
|
|
||||||
AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFLM+akEIDt75nyHZ
|
|
||||||
rbkUs/4IQWesMCkGA1UdEQQiMCCCCWxvY2FsaG9zdIINZGV2c2VydmVyLmxhbocE
|
|
||||||
wKgBZjANBgkqhkiG9w0BAQsFAAOCAYEAi4pT3LNzoM5mmD3ujX+G+cXx8MOAljvR
|
|
||||||
XGrzorysNOujR79+zR5tPPyiLwFfEYASJhWlHarM8n6DG+IgARNt9PuDq0x8+oDp
|
|
||||||
V/t9grCS6eV/SQf6y7JBQrywdIBgfYGJbbx6xzVlj187M+7yNV73f3ldS3wPFKlS
|
|
||||||
Gp6p50Vt9gmfsiqzW4NDr0FIvdbcBesNfr2DwRccZNgNAcLsr9ys2yVUcUtMnqPF
|
|
||||||
IPyarDbREuBLLLpnsYZhY3BJQgI3gDS9QRoviFCMjcbWKNnv0D2W0rVdZkJf9WOj
|
|
||||||
ro9X1K2f2C/t61EnFsM6ncqAgMksbBmlORiVi4thAgB44FDydyHX6anf7Hbzfn1t
|
|
||||||
sjsiZ03rO9xvFzUb2T9guFMBkU6pBDEhCdN3twYPY3uKPXByHnSmj3q38NJVbULu
|
|
||||||
szC1q91VF+2U2UoeoXaNxrXVleKpLiWEUJ/u/iGrbApeuov5ZTktA/y/d/+TDB+h
|
|
||||||
Jlo+Ofg3zgZBHpN+fsGTxhRLlJyXSynI
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "1aCKS1DujEPmHYGCELY7JiJ-LfxGjw7_HK1uCymlbh8",
|
"value": "dBbCt9vi-FCh7U6UBtBxlLj6LzrHM07A-Tt9g7lc5vI",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1785207783.273047,
|
"expires": 1785439751.941889,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJhNzY5YzY4Zi04MWQ5LTRiNjctODQwYS1mY2Q5NTVkNzAwNzYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc0NDI1ODN9.vdmfmIx0Dg4zKq9HTV7N7tjDbSGaO3EOeGqJbhFSOcQ",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJhZjY4NTM2Ny1hZGI1LTRiN2ItYWI2OC0zM2QzMGY4OWU1NDkiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc2NzQ1NTF9.gvhv1GLxMgANWlVK9gIn1q7bJ3Krbw2qtXf7jTjV-Ig",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1777442583.273,
|
"expires": 1777674551.941821,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1777431783134}"
|
"value": "{\"type\":\"logout\",\"at\":1777663751805}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1777604583443}"
|
"value": "{\"expiresAt\":1777836552105}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "eDlr3-DaFNFoWQE3EDAybLFpw7TiR3Maj05kLIkZfu0",
|
"value": "RgOQUW4r8HHf0oRC7CkS7N_ai16ldte8wZ4vqEnbykA",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1785207783.382249,
|
"expires": 1785439751.938223,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiNGYwNmY0YTMtMGQwMC00NzJhLWJkMjQtYjM1YjdkMjg4ZDFiIiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3NDQyNTgzfQ.F2TXov3xbhFa20P5NLt6J5HfNDlY2aXlnPoSYYDJpoQ",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiOTEwNzc3MGMtOGI5MC00MmM0LWE5MmUtNGRhN2UzMmM3MTdiIiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3Njc0NTUxfQ.ywvqlx64QlwBSC9JmoWhGZv6_JFMVgyZ0VV5CHlgUEo",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1777442583.38218,
|
"expires": 1777674551.938155,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1777431783225}"
|
"value": "{\"type\":\"logout\",\"at\":1777663751795}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1777604583575}"
|
"value": "{\"expiresAt\":1777836552105}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "BGnvmJr7XNXaCsXlzh1k6RcJPIGVxac6_LBXS22gXIA",
|
"value": "4Wf4BWiljd-T-9TdZStP2DmwF1-8Gj9YTWriMyUzrVU",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1785207781.614965,
|
"expires": 1785439750.178407,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI4YmQxMWY5NC1jNWJhLTRhNmEtOGZhMi1hZjZkYzk3Y2YyMjgiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc0NDI1ODF9.WVDBt-zfwVMHCWr78maSGWAyfQZhWOSobX5oXKXLHwg",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMTdhMDQyZS0yNGNhLTQ1MzUtODZmNC1lY2FiZWYwODIyM2MiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc2NzQ1NTB9.XQxqXaYlr21DEuuKes0AFHvuWeJvlOTwFpruuhsxacs",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1777442581.61492,
|
"expires": 1777674550.178361,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1777431781473}"
|
"value": "{\"type\":\"logout\",\"at\":1777663750032}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1777604581743}"
|
"value": "{\"expiresAt\":1777836550307}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
28
frontend/localhost+1-key.pem
Normal file
28
frontend/localhost+1-key.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC3VWwSC3U5hnOx
|
||||||
|
kznjtzIKmzuWJ/BtEg+6lh9n1KSqjnF06Q2jXj3uxfQmqZBrIDhlhbt6ZfMsHZW9
|
||||||
|
lIPsGX2vSwzNhZd9AqMdgwZK7NeLh+Hd3FDTlr4xgDrt5Rb6FQHs8rg7Dh1etwfn
|
||||||
|
6ZTKE05jtiZ1PKEci59YkgwvYfrv9PYUpXMrAVNW+OHtnHuRb32zdmfQrNEqmWqb
|
||||||
|
v7UY2+FJxTSHGhYW92asbnbuctnlbRLg66p3PjGWYHoBWF0tVwaaCKkRcHqeQQu+
|
||||||
|
bfrXWY8APUo4osrI5tl6LkvmIq85zTZ9zq92l9LG+nRegwFKNHbhSB2Qf62D4YH9
|
||||||
|
X91k5b5/AgMBAAECggEAAY+aofSzBNBeP87PGV8/79MuiLLCW0tiEuagUdP/quwX
|
||||||
|
jzVExnVQ9a19zK546lCV6ldfJ0Wi8mc2FS0kimgVJ97ttvpCNfBFz0SEUzL9CtUX
|
||||||
|
WTo8/fA0oltDJS9kKLDxGUFfzDDskxff21ujxqyvaC3u2eSwQnv12V00+VpONqjN
|
||||||
|
lbpOJhKBfvzziMtgYVqqhG12d6gavLobmoz/xBeLrmg6AG6ZCdXnhZlsC4v+GRKM
|
||||||
|
WvmImHhfqB/kkVsUO5cgXMpZhRrmtByts0PzJ1Be8fwJLkXR2oJTlNf5gPYEIOqG
|
||||||
|
LMcxvJfGso5QwV9oV4ObtXhN9jXSgW5w8liyvM7LIQKBgQDNDUhan0EDFSYCJ4vt
|
||||||
|
TvDmBUIvARncZWSClFFA1/wwUUHGPMcJo5FkOB59JWhW8+QCG/3TRszLkDJUy4O2
|
||||||
|
MEGzivrEgjycxGkLZJ8KG1Jjuj14c7fCuUVTHtIJD4eBAsPfTypcuhoXl8YAHwxH
|
||||||
|
hiQbCEORnjoys9dVO1jZgHFR5wKBgQDk4rYn+KQRLW6evk5pONa8zH4iRphH4xk6
|
||||||
|
TrmMhuykJYQpK8URweBXGQpncsXbdV4z2wBjkxhWMm4Ona1iyCida2bBBkrkugM2
|
||||||
|
g0JcbVX15Embfc6Hc5AMs6ZsnfADlFQUhS6jlFeE5VXNZQJTfloeyz+ICjoj/JDi
|
||||||
|
Eu+ULShLqQKBgGxZpm/sUugUFr9wsim1WunQwYYg6M9i7Fdrk/vVpTbK2RytJOdc
|
||||||
|
/Qid9s5eI+I+ga7zp44qjTDLgyz3VSPCIBWFTLjlsK2Nw4v3oWovwbtcv/qT+vfz
|
||||||
|
+kPPt2B+SjXLhkDLjjDtTbhFxKRvw4dPxGhcV4fsugfsq84ny+0yR67lAoGAQaiH
|
||||||
|
eI/rAMJ3qTIObEDR2PcQd+SoanbLFd7fe2B5Id1hPC5CKgXjxRh505MpDvtsOpPo
|
||||||
|
WKgpoxB0Ydz5kAy7Ge1lXJnhghuaMFkXAEydDBygwOomBNUxzXL7mszzvRMfy4Mp
|
||||||
|
DePP91+SbYk8UZc9YvgLEYtdglVBepjUAT2zAYECgYAzSQC+vGDdBlSRemwy72Nm
|
||||||
|
SJ1/2VKS94cMjCLfJ7+Q/TKxbH4t4PM8mbPYM21wkWZk+vUlDtK2p1DEnoFIIFdF
|
||||||
|
f+oHJc6TpDuLAEmbbOkYv+QeXLNXC31AFlpusUYWPs/uqhq7R7Ymp61Y7XIcWH/t
|
||||||
|
SW09F1L0b28LWYZOh38aug==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
26
frontend/localhost+1.pem
Normal file
26
frontend/localhost+1.pem
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEdDCCAtygAwIBAgIQZzySCYorx8yxVddqZLN0tDANBgkqhkiG9w0BAQsFADCB
|
||||||
|
kzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTQwMgYDVQQLDCtyeWFu
|
||||||
|
QG1hY2Jvb2sudGFpbGExNDZiMy50cy5uZXQgKFJ5YW4gS2VnZWwpMTswOQYDVQQD
|
||||||
|
DDJta2NlcnQgcnlhbkBtYWNib29rLnRhaWxhMTQ2YjMudHMubmV0IChSeWFuIEtl
|
||||||
|
Z2VsKTAeFw0yNjA0MzAwMTQyMjhaFw0yODA3MzAwMTQyMjhaMF8xJzAlBgNVBAoT
|
||||||
|
Hm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTE0MDIGA1UECwwrcnlhbkBt
|
||||||
|
YWNib29rLnRhaWxhMTQ2YjMudHMubmV0IChSeWFuIEtlZ2VsKTCCASIwDQYJKoZI
|
||||||
|
hvcNAQEBBQADggEPADCCAQoCggEBALdVbBILdTmGc7GTOeO3MgqbO5Yn8G0SD7qW
|
||||||
|
H2fUpKqOcXTpDaNePe7F9CapkGsgOGWFu3pl8ywdlb2Ug+wZfa9LDM2Fl30Cox2D
|
||||||
|
Bkrs14uH4d3cUNOWvjGAOu3lFvoVAezyuDsOHV63B+fplMoTTmO2JnU8oRyLn1iS
|
||||||
|
DC9h+u/09hSlcysBU1b44e2ce5FvfbN2Z9Cs0SqZapu/tRjb4UnFNIcaFhb3Zqxu
|
||||||
|
du5y2eVtEuDrqnc+MZZgegFYXS1XBpoIqRFwep5BC75t+tdZjwA9Sjiiysjm2Xou
|
||||||
|
S+YirznNNn3Or3aX0sb6dF6DAUo0duFIHZB/rYPhgf1f3WTlvn8CAwEAAaN3MHUw
|
||||||
|
DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaA
|
||||||
|
FLM+akEIDt75nyHZrbkUs/4IQWesMC0GA1UdEQQmMCSCCWxvY2FsaG9zdIIXUnlh
|
||||||
|
bnMtTWFjQm9vay1BaXIubG9jYWwwDQYJKoZIhvcNAQELBQADggGBABF6bepj6zob
|
||||||
|
9aZI69drzaKpYZ2kGFBuK91GawcNvGWfcP69ClT0ZfFG/UP7gkPyJgn4lCR8b909
|
||||||
|
iGK9M8CZvAaNdZKtwXgfH+GNZZO+YiwymKrwX+/Vf+VgoDDdlhwtoQWX0Pn8nLL3
|
||||||
|
qQCZcg5NduxybTtlZPZ5miAPPu63AvtUAwFlV1otrQPUs0baoEbY4URBwzRYn4Si
|
||||||
|
NL3/It1PAYx6ulxc+ctEIDernaQbVzd22MWtY3hFOt/eFKSXPZfuCTk6PVmwGR/Q
|
||||||
|
+RPuDO7sGCzcSWpKEk/liMNhm3neVSXgFMouhMch0mgW3uqLV9A45MXs0+JVZcoK
|
||||||
|
cJeo1p3GjJyWgtHSEPAcKtvaYyErU9tg18/pK9hKOxdVYl/OtusmNRfLQI8Vne/e
|
||||||
|
tl7Id/hfP1lb1FAQFENEvlHHcOVeZFMgVcRnA3dV3HEaEFKXWAnYIaLVE7WPnasq
|
||||||
|
E/774GpF3ONxJ0kGvy0f4gcrnO5UHwMgtbYL88UAvxNMa6kUd9+REw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
@@ -16,7 +16,6 @@
|
|||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^27.0.0",
|
||||||
"@types/node": "^22.18.11",
|
"@types/node": "^22.18.11",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/eslint-plugin": "^1.3.23",
|
"@vitest/eslint-plugin": "^1.3.23",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
@@ -2172,19 +2171,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-basic-ssl": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^27.0.0",
|
||||||
"@types/node": "^22.18.11",
|
"@types/node": "^22.18.11",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/eslint-plugin": "^1.3.23",
|
"@vitest/eslint-plugin": "^1.3.23",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Service Worker for Reward App push notifications */
|
/* Service Worker for Chorly push notifications */
|
||||||
|
|
||||||
/* Activate immediately — no waiting for old tabs to close */
|
/* Activate immediately — no waiting for old tabs to close */
|
||||||
self.addEventListener('install', function (event) {
|
self.addEventListener('install', function (event) {
|
||||||
@@ -16,17 +16,23 @@ self.addEventListener('push', function (event) {
|
|||||||
try {
|
try {
|
||||||
payload = event.data.json()
|
payload = event.data.json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
payload = { title: 'Reward App', body: event.data.text() }
|
payload = { title: 'Chorly', body: event.data.text() }
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = payload.title || 'Reward App'
|
const title = payload.title || 'Chorly'
|
||||||
|
// Android Chrome has a bug where event.action is unreliable — only show Approve
|
||||||
|
// so tapping it always executes the correct action.
|
||||||
|
const isAndroidChrome =
|
||||||
|
/Android/.test(self.navigator.userAgent) && /Chrome\//.test(self.navigator.userAgent)
|
||||||
// Expiry-warning notifications are informational only — no action buttons
|
// Expiry-warning notifications are informational only — no action buttons
|
||||||
const actions =
|
const actions =
|
||||||
payload.type === 'chore_expiring_soon'
|
payload.type === 'chore_expiring_soon'
|
||||||
? []
|
? []
|
||||||
|
: isAndroidChrome
|
||||||
|
? [{ action: 'approve', title: 'Approve' }]
|
||||||
: [
|
: [
|
||||||
{ action: 'approve', title: 'Approve' },
|
{ action: 'approve', title: 'Approve' },
|
||||||
{ action: 'deny', title: 'Deny' },
|
{ action: 'deny', title: 'Reject' },
|
||||||
]
|
]
|
||||||
const options = {
|
const options = {
|
||||||
body: payload.body || '',
|
body: payload.body || '',
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ function handleChoreItemReady(itemId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTaskTriggered(event: Event) {
|
function handleTaskTriggered(event: Event) {
|
||||||
console.log('Task triggered, refreshing rewards list -> ', childRewardListRef.value)
|
|
||||||
const payload = event.payload as ChildTaskTriggeredEventPayload
|
const payload = event.payload as ChildTaskTriggeredEventPayload
|
||||||
if (child.value && payload.child_id == child.value.id) {
|
if (child.value && payload.child_id == child.value.id) {
|
||||||
child.value.points = payload.points
|
child.value.points = payload.points
|
||||||
@@ -633,6 +632,33 @@ function applyHighlightPulse(itemId: string) {
|
|||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle digestToken appearing in route query (e.g. SW navigates to same route with new token)
|
||||||
|
watch(
|
||||||
|
() => route.query.digestToken,
|
||||||
|
async (token) => {
|
||||||
|
if (typeof token !== 'string' || !token) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/digest-action/${token}`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
console.log('[ParentView] digest-action POST status=', res.status, 'body=', data)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ParentView] Digest action request failed:', e)
|
||||||
|
}
|
||||||
|
// Refresh chore and reward lists to reflect the action result
|
||||||
|
childChoreListRef.value?.refresh()
|
||||||
|
childRewardListRef.value?.refresh()
|
||||||
|
if (child.value?.id) {
|
||||||
|
const updated = await fetchChildData(child.value.id)
|
||||||
|
if (updated) {
|
||||||
|
child.value = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
eventBus.on('child_task_triggered', handleTaskTriggered)
|
eventBus.on('child_task_triggered', handleTaskTriggered)
|
||||||
@@ -665,12 +691,8 @@ onMounted(async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
|
||||||
const data = await res.json().catch(() => ({}))
|
|
||||||
console.warn('Digest action failed:', data.error || res.status)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Digest action request failed:', e)
|
console.warn('[ParentView] onMounted digest action request failed:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,6 +714,8 @@ onMounted(async () => {
|
|||||||
childRewardListRef.value?.scrollToItem(scrollToId)
|
childRewardListRef.value?.scrollToItem(scrollToId)
|
||||||
applyHighlightPulse(scrollToId)
|
applyHighlightPulse(scrollToId)
|
||||||
}
|
}
|
||||||
|
const { scrollTo: _s, entityType: _e, ...remainingQuery } = route.query
|
||||||
|
router.replace({ query: remainingQuery })
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,23 +1,44 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig, loadEnv } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import basicSsl from '@vitejs/plugin-basic-ssl'
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
const backendHost = env.VITE_BACKEND_HOST ?? '127.0.0.1'
|
const backendHost = env.VITE_BACKEND_HOST ?? '127.0.0.1'
|
||||||
|
|
||||||
const httpsConfig =
|
// Use a trusted mkcert cert when present; otherwise fall back to plain HTTP.
|
||||||
fs.existsSync('./key.pem') && fs.existsSync('./cert.pem')
|
// Browsers treat localhost as a secure context even over HTTP, so service
|
||||||
? { key: fs.readFileSync('./key.pem'), cert: fs.readFileSync('./cert.pem') }
|
// workers and push notifications work without any cert on localhost.
|
||||||
: undefined
|
//
|
||||||
|
// For cross-device LAN testing (e.g. from a phone), run mkcert once per
|
||||||
|
// network — the IP in the cert doesn't matter to Vite, just drop the files
|
||||||
|
// in frontend/ and they'll be picked up automatically:
|
||||||
|
//
|
||||||
|
// mkcert localhost <your-current-lan-ip>
|
||||||
|
// # files are auto-detected; no renaming needed
|
||||||
|
//
|
||||||
|
// All *.pem files are gitignored so they stay machine-local.
|
||||||
|
const httpsConfig = (() => {
|
||||||
|
// Prefer explicit key.pem / cert.pem (e.g. custom or renamed certs)
|
||||||
|
if (fs.existsSync('./key.pem') && fs.existsSync('./cert.pem'))
|
||||||
|
return { key: fs.readFileSync('./key.pem'), cert: fs.readFileSync('./cert.pem') }
|
||||||
|
// Auto-detect any mkcert-generated *-key.pem / *.pem pair in this directory.
|
||||||
|
// mkcert always names files <host>[-key].pem regardless of which IPs are included.
|
||||||
|
const keyFile = fs.readdirSync('.').find((f) => f.endsWith('-key.pem'))
|
||||||
|
if (keyFile) {
|
||||||
|
const certFile = keyFile.replace('-key.pem', '.pem')
|
||||||
|
if (fs.existsSync(certFile))
|
||||||
|
return { key: fs.readFileSync(keyFile), cert: fs.readFileSync(certFile) }
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [vue(), basicSsl()],
|
plugins: [vue()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
https: httpsConfig ?? true,
|
https: httpsConfig,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: `http://${backendHost}:5000`,
|
target: `http://${backendHost}:5000`,
|
||||||
@@ -25,7 +46,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
},
|
},
|
||||||
'/events': {
|
'/events': {
|
||||||
target: 'http://192.168.1.102:5000',
|
target: `http://${backendHost}:5000`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user