Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6b48a80
feat(onboarding): add processed avatar uploads and profile refresh
cursoragent Aug 8, 2026
d256e9d
test(onboarding): cover avatar and welcome edge cases
cursoragent Aug 8, 2026
8a50b39
fix(onboarding): align generated types and lint rules
cursoragent Aug 8, 2026
fa5583d
fix(profile): retain processed avatars as reusable options
cursoragent Aug 8, 2026
58339ef
fix(auth): grant app runtime trusted avatar writes
cursoragent Aug 8, 2026
2ddbba4
test(onboarding): cover local image processing fallback
cursoragent Aug 8, 2026
804e014
refactor(onboarding): split welcome into guided steps
cursoragent Aug 8, 2026
58b01d5
test(onboarding): cover staged welcome navigation
cursoragent Aug 8, 2026
c0775e1
refactor(onboarding): focus welcome chrome and inline subscriptions
cursoragent Aug 28, 2026
826302d
fix(onboarding): keep welcome contract copy within schema limits
cursoragent Aug 28, 2026
6a33be8
copy(onboarding): warm welcome guidance and academic portal links
cursoragent Aug 28, 2026
c19fc5b
feat(ux): rewrite welcome import and next-steps copy
cursoragent Aug 28, 2026
b535c35
feat(ux): tweak welcome import and next-steps copy
cursoragent Aug 28, 2026
a22ca4c
chore: merge main into welcome onboarding branch
cursoragent Aug 28, 2026
9226a9f
chore: merge main into welcome onboarding branch
cursoragent Sep 3, 2026
6b31451
fix(welcome): derive selected import count for subscriptions step
cursoragent Sep 3, 2026
128d404
fix(shell): hide compact topbar brand below 320px
cursoragent Sep 3, 2026
66fb2c5
fix(weather): return 503 when every provider is unavailable
cursoragent Sep 3, 2026
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
4 changes: 2 additions & 2 deletions docs/contracts/_ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"Context Tabs": "* Not used for course, section, or teacher catalog detail pages (see Detail Two-Column Stream).\n* May still appear on workspace surfaces that switch distinct task contexts (for example dashboard calendar/homework branches).\n* When present, context navigation is a horizontal, touch-scrollable row with an overflow affordance on mobile and a vertical sidebar on larger screens; the active destination remains programmatically identified and scrolls into view.",
"Global Search Results": "* Catalog group order is sections → teachers → courses → links, then signed-in homeworks and todos.\n* Section items use title `course.namePrimary · teachers` (fall back to course name · section code when teachers are empty) and description `semester · campus · section.code` with code last.\n* Course items use title `course.namePrimary` and description `course.code`.\n* Teacher items use title `teacher.namePrimary` and description department or code.",
"Calendar Export Dialog": "* Used to display single-object iCal and personal subscription iCal.\n* Presented as a copyable link; users are not required to manually construct the URL.",
"Shell Layout": "* Desktop sidebar and main content scroll independently.\n* Signed-in desktop navigation keeps task links in Workspace; Explore and Admin tools are progressive groups.\n* A destination row is either a link or a disclosure, never both.\n* Signed-in mobile navigation fixes Today, Calendar, Tasks, and Explore above content and reserves its height and safe area.\n* Language and theme stay in the topbar; signed-in profile actions stay in the sidebar footer.\n* Anonymous topbars retain Sign in.\n* Mobile topbar/navigation controls are at least 44 by 44 CSS pixels.\n* Selected system/light/dark themes apply before first paint; system follows live OS changes with a no-JavaScript fallback.",
"Workspace Footer Policy": "* Public discovery, content, policy, and marketing pages retain the public footer.\n* Signed-in workspace roots and their settings/admin branches do not append the public footer.\n* A signed-in user visiting a public content route still sees the public footer.",
"Shell Layout": "* Desktop sidebar and main content scroll independently.\n* Signed-in desktop navigation keeps task links in Workspace; Explore and Admin tools are progressive groups.\n* A destination row is either a link or a disclosure, never both.\n* Signed-in mobile navigation pins Today, Calendar, Tasks, and Explore and reserves that height.\n* Language and theme stay in the topbar; signed-in profile actions stay in the sidebar footer.\n* Welcome hides sidebar, search, and nav.\n* Anonymous topbars retain Sign in.\n* Mobile topbar/navigation controls are at least 44 by 44 CSS pixels.\n* Selected system/light/dark themes apply before first paint; system follows live OS changes with a no-JavaScript fallback.",
"Workspace Footer Policy": "* Public discovery, content, policy, and marketing pages retain the public footer.\n* Signed-in workspace roots, onboarding (`/account/welcome`), and their settings/admin branches do not append the public footer.\n* A signed-in user visiting a public content route still sees the public footer.",
"Workspace Empty States": "* Filter-caused empty results explain the active filter and provide a direct clear-filter action.\n* Setup-caused empty states pair their explanation with the existing setup or create action.\n* Routine zero-item states remain compact and do not grow merely to fill the workspace.",
"Settings Navigation": "* Settings use concise labels in a desktop rail and scrollable mobile row.\n* `/account/settings/profile` is the default; legacy tab inputs only redirect.\n* Legacy appearance, language, and preferences values redirect to `/account/settings/preferences`.\n* Preferences reuse the pre-paint/localStorage theme mechanism and `/api/account/preferences`; URLs have no locale segments.\n* Authorizations remain distinct from linked sign-in accounts and require confirmation before revocation.\n* The active section owns its copy, exposes one aria-current page link, and scrolls into view on mobile.\n* Danger stays separate from routine settings.",
"Contribution Heatmap": "* Public profile contribution history keeps a readable minimum cell size and scrolls horizontally when needed.\n* Each contribution day exposes its localized date/count to keyboard, touch, and assistive-technology users.",
Expand Down
11 changes: 9 additions & 2 deletions docs/contracts/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"debug-password-boundary": "Email/password authentication is enabled only for local development or the explicit non-production E2E debug mode; production does not expose debug password authentication.",
"oauth-callback-integrity": "On a canonical same-origin deployment, OAuth login callbacks must complete state validation and session creation directly, without losing state due to proxy wrapping or container-internal host differences.",
"welcome-flow-required": "New users who have not set a name or username on first login must complete the welcome flow before proceeding.",
"welcome-staged-steps": "Welcome renders one URL step at a time: required profile, skippable subscriptions, then orientation. Incomplete profiles always land on profile; completed ones never return to it. Only the last step leaves for the callback. Welcome hides sidebar, search, and mobile primary nav. Subscriptions cannot read official enrollment; they inline undergraduate and graduate academic-system links and match pasted course codes.",
"welcome-avatar-upload": "During welcome, signed-in users may upload a JPG, PNG, WebP, or AVIF avatar up to 5 MB. The Worker uses the Cloudflare Images binding to auto-crop it to a 256-by-256 WebP before storing it in private R2; public avatar delivery is limited to URLs currently referenced by that user's profile.",
"welcome-oauth-refresh": "A user may reauthorize an already linked OAuth provider during welcome to fetch current upstream name and avatar options. Refreshed upstream values fill missing data and add avatar choices without overwriting profile values the user entered.",
"welcome-completion-resume": "If the welcome flow was reached from an app-relative callbackUrl, successful completion returns to that page; otherwise completion returns home.",
"auth-callback-not-intercepted": "Authorization callback continuation requests carrying OAuth code/error and state must be allowed to complete the protocol redirect; the welcome page interceptor must not break the authorization result redirect.",
"post-login-redirect": "After successful login, the user should be redirected back to the originally requested app-relative page whenever possible; unsafe external callbacks fall back to the home page.",
Expand Down Expand Up @@ -62,8 +65,12 @@
"user.username (Current value display)",
"user.image (Current avatar)",
"user.profilePictures[] (Avatar selector grid)",
"semesters[] (Semester dropdown options)",
"defaultSemesterId (Preselected semester)"
"Custom avatar upload (server-cropped 256x256 WebP in R2)",
"Linked OAuth providers (reauthorize to refresh upstream profile options)",
"Step progress indicator with back, skip, and finish controls",
"semesters[] (Semester dropdown options, subscriptions step)",
"defaultSemesterId (Preselected semester)",
"Platform guidance for subscriptions, workspace, planning, and account security (final step)"
]
}
},
Expand Down
39 changes: 34 additions & 5 deletions messages/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,8 @@
"editProfile": "Edit Profile",
"editProfileDescription": "Make changes to your profile here. Click save when you're done.",
"profilePicture": "Profile Picture",
"avatarUpload": "Upload your own avatar",
"avatarUploadHint": "JPG, PNG, WebP, or AVIF up to 5 MB. Cloudflare center-crops it to 256×256 and converts it to WebP.",
"name": "Name",
"namePlaceholder": "Your name",
"nameRequired": "Name is required.",
Expand Down Expand Up @@ -2406,14 +2408,41 @@
"description": "Set your name and username to continue.",
"firstSignIn": "First sign-in",
"continue": "Continue",
"stepProgress": "Step {current} of {total}",
"stepProfile": "Your profile",
"stepSubscriptions": "Section subscriptions",
"stepFinish": "Get started",
"back": "Back",
"skipForNow": "Skip for now",
"startUsing": "Go to workspace",
"finishTitle": "Next",
"finishDescription": "Life @ USTC covers a lot of last-mile work. You can review USTC information in one place here — try the features below!",
"success": "Profile completed",
"successDescription": "Welcome! Your profile has been set up.",
"nextStepsTitle": "Build your workspace",
"nextStepsDescription": "After saving your profile, you can browse sections, jump in from courses, or match section codes in bulk.",
"browseSections": "Browse Sections",
"browseCourses": "Browse Courses",
"nextStepsTitle": "Import your course selection from the academic system",
"nextStepsDescriptionBefore": "Because of technical limits, before using this platform you still need to copy this semester's courses from the academic system (undergraduate: ",
"nextStepsDescriptionBetween": " / graduate: ",
"nextStepsDescriptionAfter": "). Audited courses and courses you TA can go here too.",
"nextStepsPasteHint": "Paste anything that includes course codes (for example COMP3001.01) below, then click Import.",
"subscriptionsUndergraduatePortal": "Undergraduate academic system",
"subscriptionsGraduatePortal": "Graduate academic system",
"importButton": "Import",
"avatarLater": "You can add uploaded avatars later from settings.",
"bulkImportCta": "Bulk Add Subscriptions",
"avatarUploadInvalid": "Choose a valid JPG, PNG, WebP, or AVIF image.",
"avatarUploadTooLarge": "Avatar files must be 5 MB or smaller.",
"avatarUploadUnavailable": "Avatar processing is temporarily unavailable. Try again later or use an upstream avatar.",
"oauthRefreshTitle": "Refresh from a sign-in account",
"oauthRefreshDescription": "Reauthorizing fetches the latest upstream name and avatar as options without overwriting details you entered yourself.",
"oauthRefreshAction": "Refresh from {provider}",
"oauthRefreshSuccess": "Latest upstream details received. Review your name and avatar, then continue.",
"oauthRefreshFailed": "We couldn't refresh details from that sign-in account. Please try again.",
"oauthRefreshNotLinked": "That sign-in account is not linked to the current user.",
"guideWorkspaceTitle": "Review your daily items in the workspace",
"guideWorkspaceDescription": "Timetable, homework, exams, and todos are all gathered for you.",
"guideProfileTitle": "Update your profile and use the platform as your personal homepage",
"guideProfileDescription": "If you're a TA, you can also use it as a course homepage.",
"guideAppsTitle": "Use Life @ USTC from other apps",
"guideAppsDescription": "Try exporting a CalDAV link, connecting MCP, and more.",
"sectionCodesLabel": "Section codes",
"confirmImportTitle": "Confirm Section Subscriptions",
"matchedSummary": "{matched} matched, {unmatched} unmatched.",
Expand Down
39 changes: 34 additions & 5 deletions messages/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,8 @@
"editProfile": "编辑资料",
"editProfileDescription": "在此处更改您的个人资料。完成后点击保存。",
"profilePicture": "头像",
"avatarUpload": "上传自己的头像",
"avatarUploadHint": "支持 JPG、PNG、WebP 或 AVIF,最大 5 MB。图片会在 Cloudflare 中心裁切为 256×256 并转换为 WebP。",
"name": "姓名",
"namePlaceholder": "您的姓名",
"nameRequired": "请输入姓名。",
Expand Down Expand Up @@ -2276,14 +2278,41 @@
"description": "请设置您的姓名和用户名以继续使用。",
"firstSignIn": "首次登录",
"continue": "继续",
"stepProgress": "第 {current} 步,共 {total} 步",
"stepProfile": "个人资料",
"stepSubscriptions": "教学班订阅",
"stepFinish": "开始使用",
"back": "上一步",
"skipForNow": "暂时跳过",
"startUsing": "进入工作区",
"finishTitle": "接下来",
"finishDescription": "Life @ USTC 上提供了很多最后一公里的工作!你可以在这里集中查看来自科大的各项信息,先尝试一下下面的功能吧!",
"success": "信息已完善",
"successDescription": "欢迎!您的个人信息已设置完成。",
"nextStepsTitle": "继续完善你的学习工作区",
"nextStepsDescription": "保存资料后,你可以先浏览班级、从课程进入,或批量匹配班级代码。",
"browseSections": "浏览班级",
"browseCourses": "浏览课程",
"nextStepsTitle": "从教务系统导入选课",
"nextStepsDescriptionBefore": "由于技术限制,在使用本平台之前,还需要你去教务系统(本科:",
"nextStepsDescriptionBetween": " / 研究生:",
"nextStepsDescriptionAfter": ")上复制一下这学期你都选了哪些课程(其实旁听的课程、带助教的课程也都可以整理到这里哦)",
"nextStepsPasteHint": "请将包含课程代码(例如 COMP3001.01)的信息粘贴到下面,点击导入即可",
"subscriptionsUndergraduatePortal": "本科生教务",
"subscriptionsGraduatePortal": "研究生教务",
"importButton": "导入",
"avatarLater": "你可以稍后在设置中添加上传过的头像。",
"bulkImportCta": "批量添加订阅",
"avatarUploadInvalid": "请选择有效的 JPG、PNG、WebP 或 AVIF 图片。",
"avatarUploadTooLarge": "头像文件不能超过 5 MB。",
"avatarUploadUnavailable": "头像处理服务暂时不可用,请稍后重试或先使用上游头像。",
"oauthRefreshTitle": "从登录账户更新资料",
"oauthRefreshDescription": "重新授权后会获取最新的上游姓名和头像作为可选资料,不会覆盖你已经手动填写的内容。",
"oauthRefreshAction": "从 {provider} 重新获取",
"oauthRefreshSuccess": "已获取最新的上游资料,请检查姓名和头像后继续。",
"oauthRefreshFailed": "暂时无法从该登录账户更新资料,请稍后重试。",
"oauthRefreshNotLinked": "该登录账户未与当前用户关联。",
"guideWorkspaceTitle": "在工作区集中查看你的每日事项",
"guideWorkspaceDescription": "课表、作业、考试、待办都为你整理在一起了",
"guideProfileTitle": "更新一下你的个人信息,把平台作为你的个人主页来使用",
"guideProfileDescription": "如果你是助教的话也可以当成课程主页来使用",
"guideAppsTitle": "通过其他应用使用 Life @ USTC",
"guideAppsDescription": "可以尝试导出 CalDAV 链接、接入 MCP 等在其他平台使用 Life @ USTC",
"sectionCodesLabel": "班级代码",
"confirmImportTitle": "确认添加教学班订阅",
"matchedSummary": "已匹配 {matched} 个,未匹配 {unmatched} 个。",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- OAuth hooks and profile completion append trusted, server-derived avatar
-- URLs. Keep Better Auth's role read-only for this field; only the app runtime
-- receives the narrow column-level write privilege.

DO $grant_profile_picture_app_runtime$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_roles WHERE rolname = 'life_ustc_runtime'
) THEN
RAISE NOTICE 'Skipping profilePictures grant; role life_ustc_runtime does not exist.';
RETURN;
END IF;

EXECUTE
'GRANT UPDATE ("profilePictures") ON TABLE "User" TO life_ustc_runtime';
END
$grant_profile_picture_app_runtime$;
2 changes: 1 addition & 1 deletion prisma/roles/app-runtime-table-grants.sql
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,5 @@ GRANT INSERT, UPDATE ON TABLE "BusCampus", "BusRoute"
TO life_ustc_runtime;
GRANT INSERT, DELETE ON TABLE "BusRouteStop", "BusTrip"
TO life_ustc_runtime;
GRANT UPDATE ("name", "username", "isAdmin", "calendarFeedToken", "updatedAt") ON TABLE "User"
GRANT UPDATE ("name", "username", "profilePictures", "isAdmin", "calendarFeedToken", "updatedAt") ON TABLE "User"
TO life_ustc_runtime;
19 changes: 19 additions & 0 deletions public/openapi.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,25 @@
}
}
}
},
"503": {
"description": "Error response",
"headers": {
"Retry-After": {
"description": "Seconds before retrying the mutation",
"schema": {
"type": "integer",
"minimum": 0
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/openApiErrorSchema"
}
}
}
}
}
}
Expand Down
Loading