diff --git a/.github/specs/feat-notify-ending-chore.md b/.github/specs/feat-notify-ending-chore.md index 529b027..301ae59 100644 --- a/.github/specs/feat-notify-ending-chore.md +++ b/.github/specs/feat-notify-ending-chore.md @@ -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`: multiple chores → generic title, child_id/entity_id null - [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`: no push when no chores expiring - [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) -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 @@ -143,6 +143,6 @@ No automated tests added. The `sw.js` change is a simple conditional and is cove ### 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 multi-chore notification navigates to `/parent` diff --git a/.vscode/launch.json b/.vscode/launch.json index c691ebf..73c5e11 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,8 @@ "REFRESH_TOKEN_EXPIRY_DAYS": "90", "DIGEST_TOKEN_SECRET": "dev-digest-token", "VAPID_PUBLIC_KEY": "BNKkHdq45uLigohSG7c1TwlAo7ETncoRVLQK02LxHgu2P1DgSJD9njRMfbbzUsaTQGllvLBz7An1WiWsNYQhvKE", - "VAPID_PRIVATE_KEY": "jNiZJT0UO4H861KmnCt874Fg6p5jDAyYKS4V2MZf8bQ" + "VAPID_PRIVATE_KEY": "jNiZJT0UO4H861KmnCt874Fg6p5jDAyYKS4V2MZf8bQ", + "FRONTEND_URL": "https://macbook:5173" }, "args": [ "run", diff --git a/frontend/.gitignore b/frontend/.gitignore index 9d423c3..ae52eaa 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -37,6 +37,9 @@ __screenshots__/ *.old +# Local dev TLS certs (machine-specific, generated by mkcert) +*.pem + # Playwright /test-results/ /playwright-report/ diff --git a/frontend/192.168.1.102+1-key.pem b/frontend/192.168.1.102+1-key.pem deleted file mode 100644 index a05b733..0000000 --- a/frontend/192.168.1.102+1-key.pem +++ /dev/null @@ -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----- diff --git a/frontend/192.168.1.102+1.pem b/frontend/192.168.1.102+1.pem deleted file mode 100644 index b64e00f..0000000 --- a/frontend/192.168.1.102+1.pem +++ /dev/null @@ -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----- diff --git a/frontend/e2e/.auth/user-cc.json b/frontend/e2e/.auth/user-cc.json index 944137f..4befb73 100644 --- a/frontend/e2e/.auth/user-cc.json +++ b/frontend/e2e/.auth/user-cc.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "1aCKS1DujEPmHYGCELY7JiJ-LfxGjw7_HK1uCymlbh8", + "value": "dBbCt9vi-FCh7U6UBtBxlLj6LzrHM07A-Tt9g7lc5vI", "domain": "localhost", "path": "/api/auth", - "expires": 1785207783.273047, + "expires": 1785439751.941889, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJhNzY5YzY4Zi04MWQ5LTRiNjctODQwYS1mY2Q5NTVkNzAwNzYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc0NDI1ODN9.vdmfmIx0Dg4zKq9HTV7N7tjDbSGaO3EOeGqJbhFSOcQ", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJhZjY4NTM2Ny1hZGI1LTRiN2ItYWI2OC0zM2QzMGY4OWU1NDkiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc2NzQ1NTF9.gvhv1GLxMgANWlVK9gIn1q7bJ3Krbw2qtXf7jTjV-Ig", "domain": "localhost", "path": "/", - "expires": 1777442583.273, + "expires": 1777674551.941821, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,11 +27,11 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1777431783134}" + "value": "{\"type\":\"logout\",\"at\":1777663751805}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1777604583443}" + "value": "{\"expiresAt\":1777836552105}" } ] } diff --git a/frontend/e2e/.auth/user-delete.json b/frontend/e2e/.auth/user-delete.json index c609056..8cb5fc7 100644 --- a/frontend/e2e/.auth/user-delete.json +++ b/frontend/e2e/.auth/user-delete.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "eDlr3-DaFNFoWQE3EDAybLFpw7TiR3Maj05kLIkZfu0", + "value": "RgOQUW4r8HHf0oRC7CkS7N_ai16ldte8wZ4vqEnbykA", "domain": "localhost", "path": "/api/auth", - "expires": 1785207783.382249, + "expires": 1785439751.938223, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiNGYwNmY0YTMtMGQwMC00NzJhLWJkMjQtYjM1YjdkMjg4ZDFiIiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3NDQyNTgzfQ.F2TXov3xbhFa20P5NLt6J5HfNDlY2aXlnPoSYYDJpoQ", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiOTEwNzc3MGMtOGI5MC00MmM0LWE5MmUtNGRhN2UzMmM3MTdiIiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3Njc0NTUxfQ.ywvqlx64QlwBSC9JmoWhGZv6_JFMVgyZ0VV5CHlgUEo", "domain": "localhost", "path": "/", - "expires": 1777442583.38218, + "expires": 1777674551.938155, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,11 +27,11 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1777431783225}" + "value": "{\"type\":\"logout\",\"at\":1777663751795}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1777604583575}" + "value": "{\"expiresAt\":1777836552105}" } ] } diff --git a/frontend/e2e/.auth/user.json b/frontend/e2e/.auth/user.json index e9fcdcd..0d5bbba 100644 --- a/frontend/e2e/.auth/user.json +++ b/frontend/e2e/.auth/user.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "BGnvmJr7XNXaCsXlzh1k6RcJPIGVxac6_LBXS22gXIA", + "value": "4Wf4BWiljd-T-9TdZStP2DmwF1-8Gj9YTWriMyUzrVU", "domain": "localhost", "path": "/api/auth", - "expires": 1785207781.614965, + "expires": 1785439750.178407, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI4YmQxMWY5NC1jNWJhLTRhNmEtOGZhMi1hZjZkYzk3Y2YyMjgiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc0NDI1ODF9.WVDBt-zfwVMHCWr78maSGWAyfQZhWOSobX5oXKXLHwg", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMTdhMDQyZS0yNGNhLTQ1MzUtODZmNC1lY2FiZWYwODIyM2MiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc2NzQ1NTB9.XQxqXaYlr21DEuuKes0AFHvuWeJvlOTwFpruuhsxacs", "domain": "localhost", "path": "/", - "expires": 1777442581.61492, + "expires": 1777674550.178361, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,11 +27,11 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1777431781473}" + "value": "{\"type\":\"logout\",\"at\":1777663750032}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1777604581743}" + "value": "{\"expiresAt\":1777836550307}" } ] } diff --git a/frontend/localhost+1-key.pem b/frontend/localhost+1-key.pem new file mode 100644 index 0000000..5933734 --- /dev/null +++ b/frontend/localhost+1-key.pem @@ -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----- diff --git a/frontend/localhost+1.pem b/frontend/localhost+1.pem new file mode 100644 index 0000000..e36c1dc --- /dev/null +++ b/frontend/localhost+1.pem @@ -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----- diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b0aee77..5f16105 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,6 @@ "@tsconfig/node22": "^22.0.2", "@types/jsdom": "^27.0.0", "@types/node": "^22.18.11", - "@vitejs/plugin-basic-ssl": "^2.3.0", "@vitejs/plugin-vue": "^6.0.1", "@vitest/eslint-plugin": "^1.3.23", "@vue/eslint-config-prettier": "^10.2.0", @@ -2172,19 +2171,6 @@ "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": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index a87999f..756b55e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,6 @@ "@tsconfig/node22": "^22.0.2", "@types/jsdom": "^27.0.0", "@types/node": "^22.18.11", - "@vitejs/plugin-basic-ssl": "^2.3.0", "@vitejs/plugin-vue": "^6.0.1", "@vitest/eslint-plugin": "^1.3.23", "@vue/eslint-config-prettier": "^10.2.0", diff --git a/frontend/public/sw.js b/frontend/public/sw.js index d74ba1e..b49694f 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -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 */ self.addEventListener('install', function (event) { @@ -16,18 +16,24 @@ self.addEventListener('push', function (event) { try { payload = event.data.json() } 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 const actions = payload.type === 'chore_expiring_soon' ? [] - : [ - { action: 'approve', title: 'Approve' }, - { action: 'deny', title: 'Deny' }, - ] + : isAndroidChrome + ? [{ action: 'approve', title: 'Approve' }] + : [ + { action: 'approve', title: 'Approve' }, + { action: 'deny', title: 'Reject' }, + ] const options = { body: payload.body || '', icon: '/icons/icon-192x192.png', diff --git a/frontend/src/components/child/ParentView.vue b/frontend/src/components/child/ParentView.vue index e611393..e008546 100644 --- a/frontend/src/components/child/ParentView.vue +++ b/frontend/src/components/child/ParentView.vue @@ -112,7 +112,6 @@ function handleChoreItemReady(itemId: string) { } function handleTaskTriggered(event: Event) { - console.log('Task triggered, refreshing rewards list -> ', childRewardListRef.value) const payload = event.payload as ChildTaskTriggeredEventPayload if (child.value && payload.child_id == child.value.id) { child.value.points = payload.points @@ -633,6 +632,33 @@ function applyHighlightPulse(itemId: string) { }, 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 () => { try { eventBus.on('child_task_triggered', handleTaskTriggered) @@ -665,12 +691,8 @@ onMounted(async () => { method: 'POST', credentials: 'include', }) - if (!res.ok) { - const data = await res.json().catch(() => ({})) - console.warn('Digest action failed:', data.error || res.status) - } } 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) applyHighlightPulse(scrollToId) } + const { scrollTo: _s, entityType: _e, ...remainingQuery } = route.query + router.replace({ query: remainingQuery }) }, 500) } }) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 92a7816..e4a3d9e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,23 +1,44 @@ import { fileURLToPath, URL } from 'node:url' import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' -import basicSsl from '@vitejs/plugin-basic-ssl' import fs from 'fs' export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') const backendHost = env.VITE_BACKEND_HOST ?? '127.0.0.1' - const httpsConfig = - fs.existsSync('./key.pem') && fs.existsSync('./cert.pem') - ? { key: fs.readFileSync('./key.pem'), cert: fs.readFileSync('./cert.pem') } - : undefined + // Use a trusted mkcert cert when present; otherwise fall back to plain HTTP. + // Browsers treat localhost as a secure context even over HTTP, so service + // workers and push notifications work without any cert on localhost. + // + // 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 + // # 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 [-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 { - plugins: [vue(), basicSsl()], + plugins: [vue()], server: { host: '0.0.0.0', - https: httpsConfig ?? true, + https: httpsConfig, proxy: { '/api': { target: `http://${backendHost}:5000`, @@ -25,7 +46,7 @@ export default defineConfig(({ mode }) => { rewrite: (path) => path.replace(/^\/api/, ''), }, '/events': { - target: 'http://192.168.1.102:5000', + target: `http://${backendHost}:5000`, changeOrigin: true, secure: false, },