Skip to content
Merged
  •  
  •  
  •  
6 changes: 1 addition & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@
# Docker Compose app dev (`bun run dev:docker`)
# overrides DATABASE_URL and S3 endpoint to use service DNS names.
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/life_ustc_dev"
JWT_SECRET="replace-with-random-secret"
WEBHOOK_SECRET="replace-with-random-secret"
AUTH_SECRET="replace-with-random-secret"
APP_PUBLIC_ORIGIN="http://localhost:3000"
APP_CANONICAL_ORIGIN="https://life-ustc.tiankaima.dev"
BETTER_AUTH_URL="http://localhost:3000"
# Optional dedicated key for encrypting OIDC client secrets at rest.
# OIDC_CLIENT_SECRET_ENCRYPTION_KEY="replace-with-random-secret"

# Storage
# These values also drive the shared MinIO defaults used by `docker-compose.dev.yml`
Expand Down Expand Up @@ -45,7 +41,7 @@ AUTH_OIDC_CLIENT_SECRET=""
# Dev-only defaults
# UPLOAD_TOTAL_QUOTA_MB="1024"
# DEV_DEBUG_USERNAME="dev-user"
# DEV_DEBUG_NAME="Dev Debug User"
# DEV_DEBUG_NAME="Dev User"
# DEV_ADMIN_USERNAME="dev-admin"
# DEV_ADMIN_NAME="Dev Admin User"
# When E2E_DEBUG_AUTH=1 (e.g. Playwright), set both — no defaults in non-dev NODE_ENV:
Expand Down
15 changes: 7 additions & 8 deletions .github/workflows/e2e-snapshot-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ jobs:
id: commits
shell: bash
run: |
if [ "${{ github.event_name }}" = "push" ]; then
shas="$(jq -c --arg sha "$GITHUB_SHA" '[.commits[].id] | if length == 0 then [$sha] else . end' "$GITHUB_EVENT_PATH")"
else
shas="$(jq -c --arg sha "$GITHUB_SHA" '[$sha]' <<< '{}')"
fi
# Always snapshot the workflow commit only. Replaying every commit from a
# multi-commit push breaks when intermediate commits have stale tooling
# paths or transient build failures that the head commit has already fixed.
shas="$(jq -c --arg sha "$GITHUB_SHA" '[$sha]' <<< '{}')"
echo "shas=$shas" >> "$GITHUB_OUTPUT"

comment-artifacts:
Expand Down Expand Up @@ -118,7 +117,7 @@ jobs:
exit 1
fi

bun run snapshot:e2e
bun run snapshot

- name: Upload E2E snapshot artifact
id: upload
Expand Down Expand Up @@ -176,7 +175,7 @@ jobs:
if: ${{ always() }}
shell: bash
run: |
bun run tools/dev/artifacts/render-e2e-snapshot-comment.ts \
bun run tools/dev/artifacts/snapshots/render-snapshot-comment.ts \
--snapshot-dir test-results/e2e-snapshots \
--artifact-url "${{ steps.upload.outputs.artifact-url }}" \
--commit "${{ matrix.sha }}" \
Expand All @@ -191,7 +190,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
bun run tools/dev/artifacts/comment-e2e-snapshot-diff.ts \
bun run tools/dev/artifacts/snapshots/comment-snapshot-diff.ts \
--body-file test-results/e2e-snapshot-comment.md \
--commit "${{ matrix.sha }}"

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ buildPaginatedResponse(items, page, pageSize, total)
- Use `bun run verify:fast` for most commits and PR updates.
- Use `bun run check:static-import -- --baseline-ref <git-ref>` only when changing `tools/production/load/load-from-static.ts` and you need a DB-backed regression comparison against a real baseline.
- Use `bun run verify:full` before pushing changes that affect data flows, auth, browser flows, docs contracts, or shared tooling.
- Use `bun run verify:e2e` before `bun run test:e2e`; `test:e2e:bootstrap` is now just a compatibility alias.
- Use `bun run verify:e2e` before `bun run test:e2e`.

**No Stray Reports**:
- Do not leave migration plans, improvement reports, status summaries, scratch artifacts, or one-time analysis outputs in the repo.
Expand Down
1 change: 0 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ services:
PORT: 3000
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/life_ustc_dev
APP_PUBLIC_ORIGIN: http://127.0.0.1:3000
BETTER_AUTH_URL: http://127.0.0.1:3000
S3_BUCKET: *minio-dev-bucket
AWS_ENDPOINT_URL_S3: *minio-internal-endpoint
CHOKIDAR_USEPOLLING: "1"
Expand Down
1 change: 0 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ services:
APP_PUBLIC_ORIGIN: ${APP_PUBLIC_ORIGIN:?Set APP_PUBLIC_ORIGIN to the public https:// origin served by this deployment}
APP_CANONICAL_ORIGIN: ${APP_CANONICAL_ORIGIN:-}
DATABASE_URL: ${DATABASE_URL}
JWT_SECRET: ${JWT_SECRET}
WEBHOOK_SECRET: ${WEBHOOK_SECRET}
AUTH_SECRET: ${AUTH_SECRET}
S3_BUCKET: ${S3_BUCKET}
Expand Down
1 change: 1 addition & 0 deletions docs/features/_ui.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Layout Principles": "* Put reading-first content in the left/main column: introductions, descriptions, markdown, homework content, and comments.\n* Put structured facts in the right/side column: identifiers, metadata, dates, counts, actions, mini calendars, and quick links.\n* On mobile, stack in that order.\n* Reuse existing grids, cards, borders, muted text, tabs, and buttons.\n* Popups with discussion use details on the left and discussion on the right on desktop; stack on mobile.",
"List Table": "* Used for discovery-type lists such as courses, sections, and teachers.\n* Primary display information is placed in the first visual column.\n* Structured fields serve disambiguation and comparison.\n* Rows are fully clickable and navigate to the detail page.",
"Detail Hero": "* Used at the top of course, section, and teacher detail pages.\n* Contains breadcrumb, h1, and an optional subtitle.\n* h1 uses the primary display name of the current object, not an internal ID.",
"Basic Info Card": "* Used in detail page sidebars or collapsible panels.\n* Displays structured fields; does not carry comment or homework interactions.",
Expand Down
3 changes: 2 additions & 1 deletion docs/features/dashboard-link.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"link.slug visit action",
"link.group",
"link.isPinned (if authenticated)",
"Search input/query"
"Search input/query",
"Grid/list view mode persisted in browser storage"
]
}
},
Expand Down
25 changes: 11 additions & 14 deletions docs/features/homework.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"rules": {
"attached-to-section": "Homework is attached to a section, not a personal user todo.",
"no-subscription-required": "Creating homework does not require the user to be subscribed to the section first.",
"entity-and-completion-separated": "Homework entity and homework completion state are strictly separated to prevent 'I completed it' from becoming 'the homework was modified'."
"entity-and-completion-separated": "Homework entity and homework completion state are strictly separated to prevent 'I completed it' from becoming 'the homework was modified'.",
"compact-card-list-surface": "In card and list views, the default homework surface only shows title, subtitle (course name when useful plus non-default attribute badges), submission due date, relative due label, and a small completion action. Standard/default homework is not shown as a separate badge. Course/section context, description, homework timestamps, discussion, and secondary actions belong in a centered detail popup that opens on click and closes via outside click or Escape."
},
"capabilities": {
"cross-section-homework-summary": {
Expand Down Expand Up @@ -52,7 +53,9 @@
"homework.isMajor badge",
"homework.requiresTeam badge",
"completionStatus (completed/pending)",
"filter: incomplete/completed/all"
"filter: incomplete/completed/all",
"cards/list view mode persisted in browser storage",
"detail popup order: description, due summary, vertical metadata excluding platform createdAt, action controls, discussion; desktop places discussion to the right of the details"
]
}
},
Expand Down Expand Up @@ -128,12 +131,14 @@
"homework.title",
"homework.description.content",
"homework.submissionDueAt",
"homework.createdAt",
"homework.submissionStartAt",
"homework.publishedAt",
"commentCount / comments action",
"homework.publishedAt as homework publication date",
"inline homework discussion",
"user completion status",
"edit action"
"edit action",
"cards/list view mode persisted in browser storage",
"detail popup order: description, due summary, vertical metadata excluding platform createdAt, edit/completion controls, inline discussion; desktop places discussion to the right of the details",
"section cards use a responsive multi-column layout"
]
}
},
Expand Down Expand Up @@ -191,14 +196,6 @@
"notes": [
"Pass completed=true to mark as done, completed=false to revert to incomplete."
]
},
{
"name": "unset_my_homework_completion",
"returns": "{ success: Boolean, completion: { homeworkId: String, completed: Boolean, completedAt: DateTime? } }",
"rest_equivalent": "PUT /api/homeworks/[id]/completion",
"notes": [
"Dedicated tool to revert a completed homework back to incomplete; equivalent to set_my_homework_completion with completed=false."
]
}
]
},
Expand Down
4 changes: 2 additions & 2 deletions docs/features/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
"rules": {
"personal-workspace-focus": "MCP focuses by default on personal learning workspace, public query, and low-risk personal state write capabilities; admin capabilities are not exposed by default.",
"text-formatted-json": "Current tool output is uniformly text-formatted JSON.",
"output-modes": "Output mode has three levels: summary for counts/top samples, default for compact structured data, and full for exact raw records. Default is recommended for most agent calls.",
"output-modes": "Output mode has three levels: summary for counts/returned-item totals plus top samples, default for compact structured data, and full for exact raw records. Default is recommended for most agent calls.",
"coverage": "MCP currently covers profile, todos, courses, sections, teachers, semesters, subscriptions, schedules, calendar events, assistant dashboard snapshots, and bus discovery/next-trip queries; comment, upload, description governance, link management, and admin capabilities do not yet have corresponding tools.",
"aggregate-before-fanout": "Prefer assistant-oriented aggregate or filtered tools first; raw dataset tools remain available for power clients that need local post-processing.",
"privacy-safe-summary": "Summary/default outputs may omit repeated low-value nested objects and redact token-bearing URLs or other sensitive strings; full mode is the escape hatch when exact raw values are required.",
"actionable-errors": "Validation and common not-found payloads prefer plain-language messages and may include a hint that points to the next useful tool or query to recover.",
"resource-bound-access-token": "MCP transport requests must present a resource-bound Bearer token for /api/mcp. JWT access tokens minted with resource=/api/mcp are accepted; opaque tokens minted without a resource indicator are rejected because the server cannot prove MCP audience binding from those token records.",
"flexible-date-inputs": "Date and datetime parameters on MCP tools accept ISO 8601 with timezone offset (2026-05-01T08:00:00+08:00), bare date strings (2026-05-01, treated as UTC midnight for @db.Date columns), or timezone-less datetimes (2026-05-01T08:00:00, interpreted as Asia/Shanghai). Invalid strings produce a descriptive error response rather than a validation rejection.",
"time-override": "Time-sensitive tools (get_my_7days_timeline, get_upcoming_deadlines, get_my_overview, get_next_buses) accept an optional atTime parameter to anchor their internal clock to a caller-supplied moment instead of the server clock, enabling reproducible queries and future-scenario planning."
Expand Down Expand Up @@ -110,7 +111,6 @@
"tools": [
"list_my_homeworks",
"set_my_homework_completion",
"unset_my_homework_completion",
"list_my_schedules",
"list_my_exams",
"list_homeworks_by_section",
Expand Down
1 change: 1 addition & 0 deletions docs/features/oauth.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"protected-resource-canonical-path": "For protected resources with a path (currently MCP resource /api/mcp), the canonical entry per RFC 9728 should be /.well-known/oauth-protected-resource/api/mcp; the root-level /.well-known/oauth-protected-resource serves only as a compatibility alias redirect to the canonical address, to avoid returning metadata inconsistent with the resource field.",
"mcp-discovery-compatibility-aliases": "Because some MCP clients probe resource-relative or issuer-style well-known paths before settling on canonical metadata, /api/mcp/.well-known/oauth-authorization-server, /.well-known/oauth-authorization-server/api/mcp, /api/mcp/.well-known/openid-configuration, and /.well-known/openid-configuration/api/mcp should redirect to the issuer metadata used by the MCP protected resource.",
"aliases-use-redirect": "Compatibility aliases should use redirects rather than redundantly returning a JSON that looks usable but is inconsistent with issuer/resource validation, so that clients ultimately complete metadata validation at the canonical address.",
"discovery-route-targets": "Discovery metadata and compatibility aliases are wired through a shared route-target table so canonical metadata and alias redirects stay consistent when paths are added or retired.",
"discovery-cors": "Discovery metadata should support cross-origin reading; at minimum return Access-Control-Allow-Origin: * for OpenID discovery, and keep the CORS behavior of authorization server metadata and protected resource metadata consistent, reducing compatibility risks for browser-type clients and debugging tools.",
"transport-cors": "The MCP transport endpoint /api/mcp should also support browser-based clients and debugging tools using Bearer tokens: answer OPTIONS preflights, allow the MCP-specific request headers, and expose MCP-Session-Id and WWW-Authenticate on cross-origin transport responses.",
"transport-origin-validation": "Per the MCP Streamable HTTP transport guidance, /api/mcp should reject requests carrying an Origin header unless that origin matches the app's trusted origin set (public/canonical origin, localhost dev aliases, and allowed preview hosts). Non-browser clients that omit Origin remain supported."
Expand Down
1 change: 1 addition & 0 deletions docs/features/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"display": {
"fields": [
"callbackUrl query parameter (origin page)",
"Sign-in action links include the current path and query as callbackUrl",
"Fallback to home page if no origin"
]
}
Expand Down
13 changes: 13 additions & 0 deletions messages/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,9 @@
"searchShortcutHint": "Press ⌘K or Ctrl+K to focus search",
"allSitesTab": "All websites",
"overviewHint": "Overview shows 5 sites only: your pins first, then recommended ones.",
"viewMode": "Website view",
"gridView": "Grid",
"listView": "List",
"pin": "Pin",
"unpin": "Unpin",
"pinFailedTitle": "Pin update failed",
Expand Down Expand Up @@ -582,13 +585,16 @@
"startShort": "From",
"endShort": "To",
"empty": "No routes serve that stop pair in the selected direction. Try reversing the direction or picking another stop.",
"emptyReverseAction": "Reverse direction",
"departIn": "Departs in about {count} minutes",
"departEtaMinutes": "{count, plural, one {# minute} other {# minutes}}",
"departEtaHours": "{count, plural, one {# hour} other {# hours}}",
"departEtaHoursMinutes": "{hours, plural, one {# hour} other {# hours}} {minutes, plural, one {# minute} other {# minutes}}",
"etaUnknown": "ETA unavailable",
"estimatedHint": "~ marks an estimated time inferred from nearby stops on the same trip.",
"clientHint": "Day type, route matching, and ranking are computed in your browser from the raw timetable data.",
"direction": "Direction",
"routes": "Routes",
"routeSectionsCount": "{count} route sections",
"departureColumn": "Depart",
"routeColumn": "Route timetable",
Expand Down Expand Up @@ -1145,6 +1151,7 @@
"descriptionLabel": "Details",
"descriptionPlaceholder": "Add requirements, submission format, and grading notes",
"publishedAt": "Published",
"homeworkPublishedAt": "Homework published",
"submissionStart": "Submission opens",
"submissionDue": "Submission due",
"helperPublishNow": "Publish now",
Expand Down Expand Up @@ -1216,6 +1223,9 @@
"filterIncomplete": "Incomplete",
"filterCompleted": "Completed",
"filterAll": "All",
"viewMode": "Homework view",
"cardView": "Cards",
"listView": "List",
"filterEmptyTitle": "No homework under this filter",
"filterEmptyDescription": "Try another filter, or check back later.",
"addButton": "Add homework",
Expand Down Expand Up @@ -1566,6 +1576,9 @@
"previewScopeCount": "{count, plural, =0 {No scopes selected} one {# scope selected} other {# scopes selected}}",
"existingClients": "Existing Clients",
"existingClientsDescription": "Trusted first-party clients are separated from external and public clients so the admin inventory is easier to audit.",
"clientPageStatus": "Showing {start}-{end} of {total}",
"previousPage": "Previous",
"nextPage": "Next",
"tableColumnScopes": "Scopes",
"tableColumnRedirects": "Redirects",
"tableColumnActions": "Actions",
Expand Down
13 changes: 13 additions & 0 deletions messages/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,9 @@
"searchShortcutHint": "按 Ctrl+K 或 ⌘K 聚焦搜索",
"allSitesTab": "全部网站",
"overviewHint": "总览仅展示 5 个网站:优先展示你的置顶,其余按推荐补齐。",
"viewMode": "网站视图",
"gridView": "网格",
"listView": "列表",
"pin": "置顶",
"unpin": "取消置顶",
"pinFailedTitle": "置顶更新失败",
Expand Down Expand Up @@ -582,13 +585,16 @@
"startShort": "起",
"endShort": "终",
"empty": "当前方向下没有可用路线。可以尝试反向或重新选择站点。",
"emptyReverseAction": "反向查询",
"departIn": "约 {count} 分钟后发车",
"departEtaMinutes": "{count} 分钟",
"departEtaHours": "{count} 小时",
"departEtaHoursMinutes": "{hours} 小时 {minutes} 分钟",
"etaUnknown": "到站时间未知",
"estimatedHint": "~ 表示该时间由同班次相邻站点推算得出。",
"clientHint": "工作日/周末判断、路线匹配和排序都在浏览器端基于原始时刻表完成。",
"direction": "方向",
"routes": "路线",
"routeSectionsCount": "共 {count} 条路线分组",
"departureColumn": "出发",
"routeColumn": "路线时刻",
Expand Down Expand Up @@ -1122,6 +1128,7 @@
"descriptionLabel": "说明",
"descriptionPlaceholder": "补充作业要求、提交方式、评分规则等",
"publishedAt": "发布日期",
"homeworkPublishedAt": "作业发布日期",
"submissionStart": "提交开始",
"submissionDue": "提交截止",
"helperPublishNow": "立即发布",
Expand Down Expand Up @@ -1193,6 +1200,9 @@
"filterIncomplete": "未完成",
"filterCompleted": "已完成",
"filterAll": "全部",
"viewMode": "作业视图",
"cardView": "卡片",
"listView": "列表",
"filterEmptyTitle": "当前筛选下暂无作业",
"filterEmptyDescription": "切换筛选条件,或稍后再试。",
"addButton": "添加作业",
Expand Down Expand Up @@ -1543,6 +1553,9 @@
"previewScopeCount": "{count, plural, =0 {尚未选择权限} one {已选择 # 个权限} other {已选择 # 个权限}}",
"existingClients": "已有客户端",
"existingClientsDescription": "把可信第一方客户端与外部 / 公共客户端分开展示,更方便后台审计。",
"clientPageStatus": "显示第 {start}-{end} 个,共 {total} 个",
"previousPage": "上一页",
"nextPage": "下一页",
"tableColumnScopes": "权限范围",
"tableColumnRedirects": "重定向",
"tableColumnActions": "操作",
Expand Down
Loading
Loading