feat: complete M0 — legal pages, consent, tip_views metrics, account deletion UI
- /legal/terms and /legal/privacy pages (linked from sign-in) - Consent (consentGiven=true) recorded on first Google sign-in - tip_views table: one row per tip served — enables activation + reaction rate queries - tip_views purged on account deletion - Delete account button on /connect (confirm → revoke tokens → purge data → sign out) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.playwright-mcp/page-2026-04-14T14-06-03-068Z.yml
Normal file
2
.playwright-mcp/page-2026-04-14T14-06-03-068Z.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
- main [ref=e2] [cursor=pointer]:
|
||||
- generic [ref=e3]: ···
|
||||
BIN
.playwright-mcp/page-2026-04-14T14-06-13-022Z.png
Normal file
BIN
.playwright-mcp/page-2026-04-14T14-06-13-022Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
119
.playwright-mcp/page-2026-04-14T14-06-35-398Z.yml
Normal file
119
.playwright-mcp/page-2026-04-14T14-06-35-398Z.yml
Normal file
@@ -0,0 +1,119 @@
|
||||
- generic [ref=e3]:
|
||||
- generic [ref=e5]:
|
||||
- generic [ref=e8]:
|
||||
- img "Google" [ref=e10]
|
||||
- generic [ref=e11]: Sign in with Google
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e14]:
|
||||
- heading "Sign in" [level=1] [ref=e15]
|
||||
- paragraph [ref=e16]:
|
||||
- text: to continue to
|
||||
- button "alogins.net" [ref=e17] [cursor=pointer]
|
||||
- generic [ref=e18]:
|
||||
- generic [ref=e21]:
|
||||
- generic [ref=e26]:
|
||||
- textbox "Email or phone" [active] [ref=e27]
|
||||
- generic:
|
||||
- generic: Email or phone
|
||||
- paragraph [ref=e28]:
|
||||
- link "Forgot email?" [ref=e29] [cursor=pointer]:
|
||||
- /url: /signin/v2/usernamerecovery?app_domain=https://o.alogins.net&client_id=225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com&code_challenge=oJlG0iJD7GEnZ_p0tMbi56r9QGirq6lY0KHNKh-A7sI&code_challenge_method=S256&continue=https://accounts.google.com/signin/oauth/legacy/consent?authuser%3Dunknown%26part%3DAJi8hAP_s0wcqLhX3B4DTcLADDDkyjYYD_dTsSJST_xu2o-PkobLfbuk1XhXVndyBN6gkOnR0zh89fZIeMovlOB4w8i9PC5L8hRlhSDi_OSh3ki0cAZ8RRH7CMPl0NmSZbAb7253b7Sq7Uj7dN8TmsRtXQbgjYmbhyQtpViFok9UeZM7XROiV83I_xwzDvOMQD1WQkkCPL_ZjRyGIpPgmLbBXxUWwGnWs7x0CLlb2wOYMM4diy8wjIIVzACtLq0g_hnnf0y_mxrQYevSjMx1y6vLEeMZrKz4zXxSwX11OP6adWx8v9lVviJvM53GJLq4oV46GDTGcxuXLkt9W6FBgRJqMoh1oulT8tHVf1O4VG6FuQkvXnppbkH8b9OMEnqTaVlL1DVwPrvaEytsqZ79DQ74hKi9NiK3RE9wkvTaSElEARWCxILbbRjXR-GKwwybfKjQwGUZ9t2sYcJdZK3ygn8vZIYDrPIKQM8g27tJTfNj5yjjYJZsv-EMRSSvheUtOMdNBzy7fqqBKvxGQEO3iDfgy88NdOrw1AktvipSuf5E5uVlJ5KPLTq5J8TuKHO9TQcmBu9Fn1U1_NwlvMkvWbPJ2zbkshNe2q200XCnKF6Wz1bLG7sukKzEoyJkmc3x7poz1z9JQ4f7%26flowName%3DGeneralOAuthFlow%26as%3DS356240196%253A1776175595013373%26client_id%3D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%23&dsh=S356240196:1776175595013373&flowName=GeneralOAuthLite&o2v=2&opparams=%253F&rart=ANgoxceHU_HzI34dur-e3VKjuSW_62nNyS0F-NAT8vowVRgzLCH8LgeU-dR_jmgEfEI3TjNPYuzeB6EBCMum_GguMnKcbkqUwFjfpff8YscBkT3joDYra5Q&redirect_uri=https://o.alogins.net/api/auth/callback&response_type=code&scope=openid+email+profile&service=lso&state=dfjtgpxO8h1eS83EqakZj
|
||||
- generic [ref=e31]:
|
||||
- button "Next" [ref=e33]
|
||||
- link "Create account" [ref=e35] [cursor=pointer]:
|
||||
- /url: /lifecycle/flows/signup?app_domain=https://o.alogins.net&client_id=225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com&code_challenge=oJlG0iJD7GEnZ_p0tMbi56r9QGirq6lY0KHNKh-A7sI&code_challenge_method=S256&continue=https://accounts.google.com/signin/oauth/legacy/consent?authuser%3Dunknown%26part%3DAJi8hAP_s0wcqLhX3B4DTcLADDDkyjYYD_dTsSJST_xu2o-PkobLfbuk1XhXVndyBN6gkOnR0zh89fZIeMovlOB4w8i9PC5L8hRlhSDi_OSh3ki0cAZ8RRH7CMPl0NmSZbAb7253b7Sq7Uj7dN8TmsRtXQbgjYmbhyQtpViFok9UeZM7XROiV83I_xwzDvOMQD1WQkkCPL_ZjRyGIpPgmLbBXxUWwGnWs7x0CLlb2wOYMM4diy8wjIIVzACtLq0g_hnnf0y_mxrQYevSjMx1y6vLEeMZrKz4zXxSwX11OP6adWx8v9lVviJvM53GJLq4oV46GDTGcxuXLkt9W6FBgRJqMoh1oulT8tHVf1O4VG6FuQkvXnppbkH8b9OMEnqTaVlL1DVwPrvaEytsqZ79DQ74hKi9NiK3RE9wkvTaSElEARWCxILbbRjXR-GKwwybfKjQwGUZ9t2sYcJdZK3ygn8vZIYDrPIKQM8g27tJTfNj5yjjYJZsv-EMRSSvheUtOMdNBzy7fqqBKvxGQEO3iDfgy88NdOrw1AktvipSuf5E5uVlJ5KPLTq5J8TuKHO9TQcmBu9Fn1U1_NwlvMkvWbPJ2zbkshNe2q200XCnKF6Wz1bLG7sukKzEoyJkmc3x7poz1z9JQ4f7%26flowName%3DGeneralOAuthFlow%26as%3DS356240196%253A1776175595013373%26client_id%3D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%23&dsh=S356240196:1776175595013373&flowEntry=SignUp&flowName=GlifWebSignIn&o2v=2&opparams=%253F&rart=ANgoxceHU_HzI34dur-e3VKjuSW_62nNyS0F-NAT8vowVRgzLCH8LgeU-dR_jmgEfEI3TjNPYuzeB6EBCMum_GguMnKcbkqUwFjfpff8YscBkT3joDYra5Q&redirect_uri=https://o.alogins.net/api/auth/callback&response_type=code&scope=openid+email+profile&service=lso&signInUrl=https://accounts.google.com/signin/oauth?app_domain%3Dhttps://o.alogins.net%26client_id%3D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%26code_challenge%3DoJlG0iJD7GEnZ_p0tMbi56r9QGirq6lY0KHNKh-A7sI%26code_challenge_method%3DS256%26continue%3Dhttps://accounts.google.com/signin/oauth/legacy/consent?authuser%253Dunknown%2526part%253DAJi8hAP_s0wcqLhX3B4DTcLADDDkyjYYD_dTsSJST_xu2o-PkobLfbuk1XhXVndyBN6gkOnR0zh89fZIeMovlOB4w8i9PC5L8hRlhSDi_OSh3ki0cAZ8RRH7CMPl0NmSZbAb7253b7Sq7Uj7dN8TmsRtXQbgjYmbhyQtpViFok9UeZM7XROiV83I_xwzDvOMQD1WQkkCPL_ZjRyGIpPgmLbBXxUWwGnWs7x0CLlb2wOYMM4diy8wjIIVzACtLq0g_hnnf0y_mxrQYevSjMx1y6vLEeMZrKz4zXxSwX11OP6adWx8v9lVviJvM53GJLq4oV46GDTGcxuXLkt9W6FBgRJqMoh1oulT8tHVf1O4VG6FuQkvXnppbkH8b9OMEnqTaVlL1DVwPrvaEytsqZ79DQ74hKi9NiK3RE9wkvTaSElEARWCxILbbRjXR-GKwwybfKjQwGUZ9t2sYcJdZK3ygn8vZIYDrPIKQM8g27tJTfNj5yjjYJZsv-EMRSSvheUtOMdNBzy7fqqBKvxGQEO3iDfgy88NdOrw1AktvipSuf5E5uVlJ5KPLTq5J8TuKHO9TQcmBu9Fn1U1_NwlvMkvWbPJ2zbkshNe2q200XCnKF6Wz1bLG7sukKzEoyJkmc3x7poz1z9JQ4f7%2526flowName%253DGeneralOAuthFlow%2526as%253DS356240196%25253A1776175595013373%2526client_id%253D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%2523%26dsh%3DS356240196:1776175595013373%26flowName%3DGeneralOAuthLite%26o2v%3D2%26opparams%3D%25253F%26rart%3DANgoxceHU_HzI34dur-e3VKjuSW_62nNyS0F-NAT8vowVRgzLCH8LgeU-dR_jmgEfEI3TjNPYuzeB6EBCMum_GguMnKcbkqUwFjfpff8YscBkT3joDYra5Q%26redirect_uri%3Dhttps://o.alogins.net/api/auth/callback%26response_type%3Dcode%26scope%3Dopenid%2Bemail%2Bprofile%26service%3Dlso%26state%3DdfjtgpxO8h1eS83EqakZj&state=dfjtgpxO8h1eS83EqakZj
|
||||
- contentinfo [ref=e36]:
|
||||
- combobox [ref=e39] [cursor=pointer]:
|
||||
- option "Afrikaans"
|
||||
- option "azərbaycan"
|
||||
- option "bosanski"
|
||||
- option "català"
|
||||
- option "Čeština"
|
||||
- option "Cymraeg"
|
||||
- option "Dansk"
|
||||
- option "Deutsch"
|
||||
- option "eesti"
|
||||
- option "English (United Kingdom)"
|
||||
- option "English (United States)" [selected]
|
||||
- option "Español (España)"
|
||||
- option "Español (Latinoamérica)"
|
||||
- option "euskara"
|
||||
- option "Filipino"
|
||||
- option "Français (Canada)"
|
||||
- option "Français (France)"
|
||||
- option "Gaeilge"
|
||||
- option "galego"
|
||||
- option "Hrvatski"
|
||||
- option "Indonesia"
|
||||
- option "isiZulu"
|
||||
- option "íslenska"
|
||||
- option "Italiano"
|
||||
- option "Kiswahili"
|
||||
- option "latviešu"
|
||||
- option "lietuvių"
|
||||
- option "magyar"
|
||||
- option "Melayu"
|
||||
- option "Nederlands"
|
||||
- option "norsk"
|
||||
- option "o‘zbek"
|
||||
- option "polski"
|
||||
- option "Português (Brasil)"
|
||||
- option "Português (Portugal)"
|
||||
- option "română"
|
||||
- option "shqip"
|
||||
- option "Slovenčina"
|
||||
- option "slovenščina"
|
||||
- option "srpski (latinica)"
|
||||
- option "Suomi"
|
||||
- option "Svenska"
|
||||
- option "Tiếng Việt"
|
||||
- option "Türkçe"
|
||||
- option "Ελληνικά"
|
||||
- option "беларуская"
|
||||
- option "български"
|
||||
- option "кыргызча"
|
||||
- option "қазақ тілі"
|
||||
- option "македонски"
|
||||
- option "монгол"
|
||||
- option "Русский"
|
||||
- option "српски (ћирилица)"
|
||||
- option "Українська"
|
||||
- option "ქართული"
|
||||
- option "հայերեն"
|
||||
- option "עברית"
|
||||
- option "اردو"
|
||||
- option "العربية"
|
||||
- option "فارسی"
|
||||
- option "አማርኛ"
|
||||
- option "नेपाली"
|
||||
- option "मराठी"
|
||||
- option "हिन्दी"
|
||||
- option "অসমীয়া"
|
||||
- option "বাংলা"
|
||||
- option "ਪੰਜਾਬੀ"
|
||||
- option "ગુજરાતી"
|
||||
- option "ଓଡ଼ିଆ"
|
||||
- option "தமிழ்"
|
||||
- option "తెలుగు"
|
||||
- option "ಕನ್ನಡ"
|
||||
- option "മലയാളം"
|
||||
- option "සිංහල"
|
||||
- option "ไทย"
|
||||
- option "ລາວ"
|
||||
- option "မြန်မာ"
|
||||
- option "ខ្មែរ"
|
||||
- option "한국어"
|
||||
- option "中文(香港)"
|
||||
- option "日本語"
|
||||
- option "简体中文"
|
||||
- option "繁體中文"
|
||||
- list [ref=e40]:
|
||||
- listitem [ref=e41]:
|
||||
- link "Help" [ref=e42] [cursor=pointer]:
|
||||
- /url: https://support.google.com/accounts?hl=en-US&p=account_iph
|
||||
- listitem [ref=e43]:
|
||||
- link "Privacy" [ref=e44] [cursor=pointer]:
|
||||
- /url: https://accounts.google.com/TOS?loc=LV&hl=en-US&privacy=true
|
||||
- listitem [ref=e45]:
|
||||
- link "Terms" [ref=e46] [cursor=pointer]:
|
||||
- /url: https://accounts.google.com/TOS?loc=LV&hl=en-US
|
||||
16
.playwright-mcp/page-2026-04-14T14-12-07-158Z.yml
Normal file
16
.playwright-mcp/page-2026-04-14T14-12-07-158Z.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
- main [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- heading "oO" [level=1] [ref=e4]
|
||||
- paragraph [ref=e5]: one tip. right now.
|
||||
- link "Continue with Google" [ref=e7] [cursor=pointer]:
|
||||
- /url: /api/auth/login?redirectTo=/connect
|
||||
- img [ref=e8]
|
||||
- text: Continue with Google
|
||||
- paragraph [ref=e13]:
|
||||
- text: By continuing you agree to our
|
||||
- link "Terms" [ref=e14] [cursor=pointer]:
|
||||
- /url: /legal/terms
|
||||
- text: and
|
||||
- link "Privacy Policy" [ref=e15] [cursor=pointer]:
|
||||
- /url: /legal/privacy
|
||||
- text: .
|
||||
2
.playwright-mcp/page-2026-04-14T14-12-17-872Z.yml
Normal file
2
.playwright-mcp/page-2026-04-14T14-12-17-872Z.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
- main [ref=e2] [cursor=pointer]:
|
||||
- generic [ref=e3]: ···
|
||||
16
.playwright-mcp/page-2026-04-14T14-14-53-788Z.yml
Normal file
16
.playwright-mcp/page-2026-04-14T14-14-53-788Z.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
- main [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- heading "oO" [level=1] [ref=e4]
|
||||
- paragraph [ref=e5]: one tip. right now.
|
||||
- link "Continue with Google" [ref=e7] [cursor=pointer]:
|
||||
- /url: /api/auth/login?redirectTo=/connect
|
||||
- img [ref=e8]
|
||||
- text: Continue with Google
|
||||
- paragraph [ref=e13]:
|
||||
- text: By continuing you agree to our
|
||||
- link "Terms" [ref=e14] [cursor=pointer]:
|
||||
- /url: /legal/terms
|
||||
- text: and
|
||||
- link "Privacy Policy" [ref=e15] [cursor=pointer]:
|
||||
- /url: /legal/privacy
|
||||
- text: .
|
||||
119
.playwright-mcp/page-2026-04-14T14-26-57-754Z.yml
Normal file
119
.playwright-mcp/page-2026-04-14T14-26-57-754Z.yml
Normal file
@@ -0,0 +1,119 @@
|
||||
- generic [ref=e3]:
|
||||
- generic [ref=e5]:
|
||||
- generic [ref=e8]:
|
||||
- img "Google" [ref=e10]
|
||||
- generic [ref=e11]: Sign in with Google
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e14]:
|
||||
- heading "Sign in" [level=1] [ref=e15]
|
||||
- paragraph [ref=e16]:
|
||||
- text: to continue to
|
||||
- button "alogins.net" [ref=e17] [cursor=pointer]
|
||||
- generic [ref=e18]:
|
||||
- generic [ref=e21]:
|
||||
- generic [ref=e26]:
|
||||
- textbox "Email or phone" [active] [ref=e27]
|
||||
- generic:
|
||||
- generic: Email or phone
|
||||
- paragraph [ref=e28]:
|
||||
- link "Forgot email?" [ref=e29] [cursor=pointer]:
|
||||
- /url: /signin/v2/usernamerecovery?app_domain=https://o.alogins.net&client_id=225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com&code_challenge=-BqTqiuiRm7bqR18AN6dGJza3m1LQ-rUHm5iCKVp4L0&code_challenge_method=S256&continue=https://accounts.google.com/signin/oauth/legacy/consent?authuser%3Dunknown%26part%3DAJi8hAMZa_8Uw5oeDBf5xu6LwYAzWw0IfUPVe1s-RdkhKrryVpbj5DP1e4mWD5shlmUM9MHWJjmo0221X4Itv9m_QVhq6U9AE3ixMmFjYikcxLUbYpSC0ZK2Gpk6iXyS0bmfDCxbgrB-XxcJUfHUxxHIFAIPwGNH1HXRh9xm5nfY7qXSpWvjqH2SjPsvABBxBfb4_gYQVYDpFH6xY9tvo9ivQm607DUHzOGvM91l2PAzDK2xfzP1ly9SqBaA342VjnFJmEA5mKtYKfGXiThy7J62jQMM-NK8pjUTysf6PXNOLLyUCtl7VjQPs23EBAwe_22hjTKkVo9DRb9dlv8VNmIdVK-BVUoalJmfsRs3BVet5yibkeKV20QKQlCDgtPLCorYZyu3gnFh5taleK1WBbKLyRtscF3rVnhCkIp4Y4y3m8nHtTr_lKRi0jyIfZA9CJwxJnFJa50DFwjmiOYWosdQsuu6HCZ5CfNfCorlKIYB3zjmMRcFHdwYxPvydfXXR6OPui9JQfcE4KEjnE4sDQbB6B70TD9R-o7lqt5V9Qh0cOWwKpO0-m167gCqNjS67_qgdM4UK0vGResYyPoAGae9OtyQmCAj_dcU-62wi6Keit4fc9kh4tW_Sg3z-_uWIv3YDam93LJE%26flowName%3DGeneralOAuthFlow%26as%3DS-1308179881%253A1776176817375272%26client_id%3D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%23&dsh=S-1308179881:1776176817375272&flowName=GeneralOAuthLite&o2v=2&opparams=%253F&rart=ANgoxceTKJUOKU3v1dqvsh5ic8iCtLsW59TMJ-d2ocODSbnDkgr7QsEsYbgBLhyzyxlCKvCeKxeWXE4oh50Ic8Zii472KjuAo007j0Fg5aey109I_iJ5RME&redirect_uri=https://o.alogins.net/api/auth/callback&response_type=code&scope=openid+email+profile&service=lso&state=8jVx7mQ1bRb8HljdpHf35
|
||||
- generic [ref=e31]:
|
||||
- button "Next" [ref=e33]
|
||||
- link "Create account" [ref=e35] [cursor=pointer]:
|
||||
- /url: /lifecycle/flows/signup?app_domain=https://o.alogins.net&client_id=225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com&code_challenge=-BqTqiuiRm7bqR18AN6dGJza3m1LQ-rUHm5iCKVp4L0&code_challenge_method=S256&continue=https://accounts.google.com/signin/oauth/legacy/consent?authuser%3Dunknown%26part%3DAJi8hAMZa_8Uw5oeDBf5xu6LwYAzWw0IfUPVe1s-RdkhKrryVpbj5DP1e4mWD5shlmUM9MHWJjmo0221X4Itv9m_QVhq6U9AE3ixMmFjYikcxLUbYpSC0ZK2Gpk6iXyS0bmfDCxbgrB-XxcJUfHUxxHIFAIPwGNH1HXRh9xm5nfY7qXSpWvjqH2SjPsvABBxBfb4_gYQVYDpFH6xY9tvo9ivQm607DUHzOGvM91l2PAzDK2xfzP1ly9SqBaA342VjnFJmEA5mKtYKfGXiThy7J62jQMM-NK8pjUTysf6PXNOLLyUCtl7VjQPs23EBAwe_22hjTKkVo9DRb9dlv8VNmIdVK-BVUoalJmfsRs3BVet5yibkeKV20QKQlCDgtPLCorYZyu3gnFh5taleK1WBbKLyRtscF3rVnhCkIp4Y4y3m8nHtTr_lKRi0jyIfZA9CJwxJnFJa50DFwjmiOYWosdQsuu6HCZ5CfNfCorlKIYB3zjmMRcFHdwYxPvydfXXR6OPui9JQfcE4KEjnE4sDQbB6B70TD9R-o7lqt5V9Qh0cOWwKpO0-m167gCqNjS67_qgdM4UK0vGResYyPoAGae9OtyQmCAj_dcU-62wi6Keit4fc9kh4tW_Sg3z-_uWIv3YDam93LJE%26flowName%3DGeneralOAuthFlow%26as%3DS-1308179881%253A1776176817375272%26client_id%3D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%23&dsh=S-1308179881:1776176817375272&flowEntry=SignUp&flowName=GlifWebSignIn&o2v=2&opparams=%253F&rart=ANgoxceTKJUOKU3v1dqvsh5ic8iCtLsW59TMJ-d2ocODSbnDkgr7QsEsYbgBLhyzyxlCKvCeKxeWXE4oh50Ic8Zii472KjuAo007j0Fg5aey109I_iJ5RME&redirect_uri=https://o.alogins.net/api/auth/callback&response_type=code&scope=openid+email+profile&service=lso&signInUrl=https://accounts.google.com/signin/oauth?app_domain%3Dhttps://o.alogins.net%26client_id%3D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%26code_challenge%3D-BqTqiuiRm7bqR18AN6dGJza3m1LQ-rUHm5iCKVp4L0%26code_challenge_method%3DS256%26continue%3Dhttps://accounts.google.com/signin/oauth/legacy/consent?authuser%253Dunknown%2526part%253DAJi8hAMZa_8Uw5oeDBf5xu6LwYAzWw0IfUPVe1s-RdkhKrryVpbj5DP1e4mWD5shlmUM9MHWJjmo0221X4Itv9m_QVhq6U9AE3ixMmFjYikcxLUbYpSC0ZK2Gpk6iXyS0bmfDCxbgrB-XxcJUfHUxxHIFAIPwGNH1HXRh9xm5nfY7qXSpWvjqH2SjPsvABBxBfb4_gYQVYDpFH6xY9tvo9ivQm607DUHzOGvM91l2PAzDK2xfzP1ly9SqBaA342VjnFJmEA5mKtYKfGXiThy7J62jQMM-NK8pjUTysf6PXNOLLyUCtl7VjQPs23EBAwe_22hjTKkVo9DRb9dlv8VNmIdVK-BVUoalJmfsRs3BVet5yibkeKV20QKQlCDgtPLCorYZyu3gnFh5taleK1WBbKLyRtscF3rVnhCkIp4Y4y3m8nHtTr_lKRi0jyIfZA9CJwxJnFJa50DFwjmiOYWosdQsuu6HCZ5CfNfCorlKIYB3zjmMRcFHdwYxPvydfXXR6OPui9JQfcE4KEjnE4sDQbB6B70TD9R-o7lqt5V9Qh0cOWwKpO0-m167gCqNjS67_qgdM4UK0vGResYyPoAGae9OtyQmCAj_dcU-62wi6Keit4fc9kh4tW_Sg3z-_uWIv3YDam93LJE%2526flowName%253DGeneralOAuthFlow%2526as%253DS-1308179881%25253A1776176817375272%2526client_id%253D225787180325-cesuikl95j1qg3aijh4j10n8n7usf65p.apps.googleusercontent.com%2523%26dsh%3DS-1308179881:1776176817375272%26flowName%3DGeneralOAuthLite%26o2v%3D2%26opparams%3D%25253F%26rart%3DANgoxceTKJUOKU3v1dqvsh5ic8iCtLsW59TMJ-d2ocODSbnDkgr7QsEsYbgBLhyzyxlCKvCeKxeWXE4oh50Ic8Zii472KjuAo007j0Fg5aey109I_iJ5RME%26redirect_uri%3Dhttps://o.alogins.net/api/auth/callback%26response_type%3Dcode%26scope%3Dopenid%2Bemail%2Bprofile%26service%3Dlso%26state%3D8jVx7mQ1bRb8HljdpHf35&state=8jVx7mQ1bRb8HljdpHf35
|
||||
- contentinfo [ref=e36]:
|
||||
- combobox [ref=e39] [cursor=pointer]:
|
||||
- option "Afrikaans"
|
||||
- option "azərbaycan"
|
||||
- option "bosanski"
|
||||
- option "català"
|
||||
- option "Čeština"
|
||||
- option "Cymraeg"
|
||||
- option "Dansk"
|
||||
- option "Deutsch"
|
||||
- option "eesti"
|
||||
- option "English (United Kingdom)"
|
||||
- option "English (United States)" [selected]
|
||||
- option "Español (España)"
|
||||
- option "Español (Latinoamérica)"
|
||||
- option "euskara"
|
||||
- option "Filipino"
|
||||
- option "Français (Canada)"
|
||||
- option "Français (France)"
|
||||
- option "Gaeilge"
|
||||
- option "galego"
|
||||
- option "Hrvatski"
|
||||
- option "Indonesia"
|
||||
- option "isiZulu"
|
||||
- option "íslenska"
|
||||
- option "Italiano"
|
||||
- option "Kiswahili"
|
||||
- option "latviešu"
|
||||
- option "lietuvių"
|
||||
- option "magyar"
|
||||
- option "Melayu"
|
||||
- option "Nederlands"
|
||||
- option "norsk"
|
||||
- option "o‘zbek"
|
||||
- option "polski"
|
||||
- option "Português (Brasil)"
|
||||
- option "Português (Portugal)"
|
||||
- option "română"
|
||||
- option "shqip"
|
||||
- option "Slovenčina"
|
||||
- option "slovenščina"
|
||||
- option "srpski (latinica)"
|
||||
- option "Suomi"
|
||||
- option "Svenska"
|
||||
- option "Tiếng Việt"
|
||||
- option "Türkçe"
|
||||
- option "Ελληνικά"
|
||||
- option "беларуская"
|
||||
- option "български"
|
||||
- option "кыргызча"
|
||||
- option "қазақ тілі"
|
||||
- option "македонски"
|
||||
- option "монгол"
|
||||
- option "Русский"
|
||||
- option "српски (ћирилица)"
|
||||
- option "Українська"
|
||||
- option "ქართული"
|
||||
- option "հայերեն"
|
||||
- option "עברית"
|
||||
- option "اردو"
|
||||
- option "العربية"
|
||||
- option "فارسی"
|
||||
- option "አማርኛ"
|
||||
- option "नेपाली"
|
||||
- option "मराठी"
|
||||
- option "हिन्दी"
|
||||
- option "অসমীয়া"
|
||||
- option "বাংলা"
|
||||
- option "ਪੰਜਾਬੀ"
|
||||
- option "ગુજરાતી"
|
||||
- option "ଓଡ଼ିଆ"
|
||||
- option "தமிழ்"
|
||||
- option "తెలుగు"
|
||||
- option "ಕನ್ನಡ"
|
||||
- option "മലയാളം"
|
||||
- option "සිංහල"
|
||||
- option "ไทย"
|
||||
- option "ລາວ"
|
||||
- option "မြန်မာ"
|
||||
- option "ខ្មែរ"
|
||||
- option "한국어"
|
||||
- option "中文(香港)"
|
||||
- option "日本語"
|
||||
- option "简体中文"
|
||||
- option "繁體中文"
|
||||
- list [ref=e40]:
|
||||
- listitem [ref=e41]:
|
||||
- link "Help" [ref=e42] [cursor=pointer]:
|
||||
- /url: https://support.google.com/accounts?hl=en-US&p=account_iph
|
||||
- listitem [ref=e43]:
|
||||
- link "Privacy" [ref=e44] [cursor=pointer]:
|
||||
- /url: https://accounts.google.com/TOS?loc=LV&hl=en-US&privacy=true
|
||||
- listitem [ref=e45]:
|
||||
- link "Terms" [ref=e46] [cursor=pointer]:
|
||||
- /url: https://accounts.google.com/TOS?loc=LV&hl=en-US
|
||||
16
.playwright-mcp/page-2026-04-15T07-41-23-867Z.yml
Normal file
16
.playwright-mcp/page-2026-04-15T07-41-23-867Z.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
- main [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- heading "oO" [level=1] [ref=e4]
|
||||
- paragraph [ref=e5]: one tip. right now.
|
||||
- link "Continue with Google" [ref=e7] [cursor=pointer]:
|
||||
- /url: /api/auth/login?redirectTo=/connect
|
||||
- img [ref=e8]
|
||||
- text: Continue with Google
|
||||
- paragraph [ref=e13]:
|
||||
- text: By continuing you agree to our
|
||||
- link "Terms" [ref=e14] [cursor=pointer]:
|
||||
- /url: /legal/terms
|
||||
- text: and
|
||||
- link "Privacy Policy" [ref=e15] [cursor=pointer]:
|
||||
- /url: /legal/privacy
|
||||
- text: .
|
||||
16
.playwright-mcp/page-2026-04-15T08-09-40-643Z.yml
Normal file
16
.playwright-mcp/page-2026-04-15T08-09-40-643Z.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
- main [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- heading "oO" [level=1] [ref=e4]
|
||||
- paragraph [ref=e5]: one tip. right now.
|
||||
- link "Continue with Google" [ref=e7] [cursor=pointer]:
|
||||
- /url: /api/auth/login?redirectTo=/connect
|
||||
- img [ref=e8]
|
||||
- text: Continue with Google
|
||||
- paragraph [ref=e13]:
|
||||
- text: By continuing you agree to our
|
||||
- link "Terms" [ref=e14] [cursor=pointer]:
|
||||
- /url: /legal/terms
|
||||
- text: and
|
||||
- link "Privacy Policy" [ref=e15] [cursor=pointer]:
|
||||
- /url: /legal/privacy
|
||||
- text: .
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { getIntegrations, disconnectIntegration } from '@/lib/api';
|
||||
import { getIntegrations, disconnectIntegration, deleteAccount, logout } from '@/lib/api';
|
||||
import type { Integration } from '@oo/shared-types';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
@@ -11,6 +11,7 @@ function ConnectPageInner() {
|
||||
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [disconnecting, setDisconnecting] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const { integrations: list } = await getIntegrations();
|
||||
@@ -26,6 +27,14 @@ function ConnectPageInner() {
|
||||
const isConnected = (provider: string) =>
|
||||
integrations.some((i) => i.provider === provider && i.status === 'connected');
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
if (!confirm('Delete your account? This cannot be undone.')) return;
|
||||
setDeleting(true);
|
||||
await deleteAccount();
|
||||
await logout();
|
||||
window.location.href = '/sign-in';
|
||||
};
|
||||
|
||||
const handleDisconnect = async (provider: string) => {
|
||||
setDisconnecting(provider);
|
||||
await disconnectIntegration(provider);
|
||||
@@ -140,6 +149,23 @@ function ConnectPageInner() {
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '4rem', borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: '2rem' }}>
|
||||
<button
|
||||
onClick={handleDeleteAccount}
|
||||
disabled={deleting}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'rgba(255,255,255,0.2)',
|
||||
fontSize: '0.8rem',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{deleting ? 'Deleting…' : 'Delete account'}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
41
apps/web/src/app/legal/privacy/page.tsx
Normal file
41
apps/web/src/app/legal/privacy/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
export default function Privacy() {
|
||||
return (
|
||||
<main style={{ maxWidth: '640px', margin: '0 auto', padding: '4rem 2rem', lineHeight: 1.7 }}>
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 300, marginBottom: '2rem', letterSpacing: '-0.02em' }}>Privacy Policy</h1>
|
||||
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.8rem', marginBottom: '2.5rem' }}>Effective: 1 April 2026</p>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>What we collect</h2>
|
||||
<ul style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem', paddingLeft: '1.25rem' }}>
|
||||
<li style={{ marginBottom: '0.5rem' }}>Your Google account email, name, and profile picture — to identify you.</li>
|
||||
<li style={{ marginBottom: '0.5rem' }}>OAuth tokens for integrations you explicitly connect.</li>
|
||||
<li style={{ marginBottom: '0.5rem' }}>Your reactions to tips (done / snooze / dismiss) — to improve recommendations.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>What we don't collect</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
We do not copy your tasks, calendar events, or any third-party app content into our database. Data is fetched on demand and held in memory for at most 30 seconds.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>How we use it</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
Solely to operate the recommendation engine. We do not sell data, share it with third parties, or use it for advertising.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>Your rights</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
You can disconnect any integration at any time from the Connect page. You can delete your account, which permanently purges all stored data. Contact the owner for data export requests.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<a href="/sign-in" style={{ color: 'rgba(255,255,255,0.35)', fontSize: '0.8rem' }}>← Back</a>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
39
apps/web/src/app/legal/terms/page.tsx
Normal file
39
apps/web/src/app/legal/terms/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
export default function Terms() {
|
||||
return (
|
||||
<main style={{ maxWidth: '640px', margin: '0 auto', padding: '4rem 2rem', lineHeight: 1.7 }}>
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 300, marginBottom: '2rem', letterSpacing: '-0.02em' }}>Terms of Service</h1>
|
||||
|
||||
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.8rem', marginBottom: '2.5rem' }}>Effective: 1 April 2026</p>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>1. The service</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
oO is a personal recommendation system. It reads signals from apps you connect and surfaces one tip at a time. The service is provided as-is during the prototype phase.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>2. Your data</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
We store OAuth tokens for integrations you connect. We fetch your tasks on demand — we do not copy or cache raw data beyond a 30-second in-memory buffer. You can revoke access or delete your account at any time.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>3. Account deletion</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
Deleting your account revokes all integration tokens, purges your feedback history, and anonymises your identity record. No data is retained in identifiable form.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 500, marginBottom: '0.75rem' }}>4. Limitations</h2>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
This is a prototype. We make no uptime guarantees. The service may change or be discontinued with reasonable notice.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<a href="/sign-in" style={{ color: 'rgba(255,255,255,0.35)', fontSize: '0.8rem' }}>← Back</a>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -88,6 +88,7 @@ export default function TipPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<style>{`
|
||||
@keyframes breathe {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
@@ -100,12 +101,11 @@ export default function TipPage() {
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerLeave={onPointerUp}
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
height: '100dvh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '3rem 2rem',
|
||||
userSelect: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
cursor: state === 'tip' ? 'default' : 'auto',
|
||||
@@ -127,7 +127,7 @@ export default function TipPage() {
|
||||
{/* Loading label */}
|
||||
{(state === 'loading' || state === 'done') && (
|
||||
<p style={{
|
||||
marginTop: '1.25rem',
|
||||
margin: 0,
|
||||
color: 'rgba(255,255,255,0.55)',
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.18em',
|
||||
@@ -140,7 +140,7 @@ export default function TipPage() {
|
||||
|
||||
{/* Tip */}
|
||||
{(state === 'tip' || state === 'actions') && tip && (
|
||||
<Fade visible={visible && state !== 'actions'} style={{ textAlign: 'center', maxWidth: '420px' }}>
|
||||
<Fade visible={visible && state !== 'actions'} style={{ textAlign: 'center', maxWidth: '420px', padding: '0 2rem' }}>
|
||||
<p style={{
|
||||
fontSize: 'clamp(1.25rem, 4vw, 1.75rem)',
|
||||
fontWeight: 300,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,14 @@ export const tipFeedback = sqliteTable('tip_feedback', {
|
||||
createdAt: text('created_at').notNull(),
|
||||
});
|
||||
|
||||
// Each row = one tip served. Join with tipFeedback on tipId to compute reaction rate + dwell.
|
||||
export const tipViews = sqliteTable('tip_views', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id),
|
||||
tipId: text('tip_id').notNull(),
|
||||
servedAt: text('served_at').notNull(),
|
||||
});
|
||||
|
||||
export const sessions = sqliteTable('sessions', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id),
|
||||
|
||||
@@ -103,7 +103,7 @@ router.get('/callback', async (req: Request, res: Response) => {
|
||||
|
||||
if (!user) {
|
||||
const id = nanoid();
|
||||
await db.insert(users).values({ id, email, name, image, googleId, createdAt: now });
|
||||
await db.insert(users).values({ id, email, name, image, googleId, createdAt: now, consentGiven: true, consentAt: now });
|
||||
[user] = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Router as ExpressRouter, Router, Response } from 'express';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { db } from '../db/index.js';
|
||||
import { integrationTokens, tipFeedback } from '../db/schema.js';
|
||||
import { integrationTokens, tipFeedback, tipViews } from '../db/schema.js';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { requireAuth, AuthenticatedRequest } from '../middleware/session.js';
|
||||
import type { Tip } from '@oo/shared-types';
|
||||
@@ -72,6 +72,14 @@ router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Re
|
||||
return;
|
||||
}
|
||||
|
||||
// Record metric: tip served
|
||||
await db.insert(tipViews).values({
|
||||
id: nanoid(),
|
||||
userId: req.userId!,
|
||||
tipId: tip.id,
|
||||
servedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.json({ tip });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Router as ExpressRouter, Router, Response } from 'express';
|
||||
import { db } from '../db/index.js';
|
||||
import { users, integrationTokens, tipFeedback, sessions } from '../db/schema.js';
|
||||
import { users, integrationTokens, tipFeedback, tipViews, sessions } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { requireAuth, AuthenticatedRequest } from '../middleware/session.js';
|
||||
|
||||
@@ -54,6 +54,7 @@ router.delete('/me', requireAuth, async (req: AuthenticatedRequest, res: Respons
|
||||
// Delete cascade
|
||||
await db.delete(integrationTokens).where(eq(integrationTokens.userId, userId));
|
||||
await db.delete(tipFeedback).where(eq(tipFeedback.userId, userId));
|
||||
await db.delete(tipViews).where(eq(tipViews.userId, userId));
|
||||
await db.delete(sessions).where(eq(sessions.userId, userId));
|
||||
|
||||
// Soft-delete user (GDPR: keep audit trail row without PII)
|
||||
|
||||
Reference in New Issue
Block a user