Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
| 📋 **Theo dõi deadline** | Lấy bài tập, quiz, điểm danh từ `courses.ut.edu.vn` và `thnn.ut.edu.vn` qua Moodle WS hoặc phiên web an toàn khi site không cấp WS |
| 📊 **Theo dõi điểm** | Giám sát thay đổi điểm theo thời gian thực, thông báo khi có điểm mới |
| 🔔 **Cảnh báo thông minh** | Phân loại `Khẩn cấp` · `Sắp hạn` · `An toàn` · `Quá hạn` |
| 📅 **Lịch học** | Xem lịch học theo tuần với deadline trực quan |
| 📅 **Lịch học hôm nay** | Đồng bộ lớp học từ UTH Portal với giờ học, tiết, phòng, cơ sở và trạng thái theo thời gian thực |
| ⚡ **Hiệu suất cao** | Startup ~4s, parallel API, grade N+1 optimization |
| 📱 **Đa nền tảng** | Windows MSI/EXE · Android APK · iOS IPA · Web browser |
| 🎨 **6 Theme** | Midnight Blue · Ocean Teal · Sakura Pink · Nord Frost · Monokai Pro · Solarized Dark |
Expand Down
5 changes: 3 additions & 2 deletions docs/PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ the local native-fault file.

It also does not upload a name, email address, username, account ID, password,
token, sesskey, cookie, MoodleSession, Authorization value, absolute home path,
course name, activity title, submission, grade, file content, or any other
Moodle data. Unknown fields are rejected before a report is written to disk.
course name, activity title, class schedule, room, campus, submission, grade,
file content, or any other Moodle/Portal data. Unknown fields are rejected
before a report is written to disk.

## Local files, limits, and deletion

Expand Down
54 changes: 49 additions & 5 deletions docs/api/portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
> **Status**: Maintained reference. Captures and credentials must remain outside
> Git; only redacted endpoint contracts belong in this document.

> **Source**: `https://portal.ut.edu.vn`
> **Frontend**: `https://portal.ut.edu.vn`
> **API base**: `https://portal-publicapp.ut.edu.vn/api/v1`
> **Method**: Chrome DevTools network traffic capture (live session)
> **Date**: 2026-06-22
> **Last verified**: 2026-08-11
> **Auth**: JWT Bearer Token (HS256, 30-day expiry)
> **SPA Framework**: React Router
> **Backend**: Spring Boot (Java) — inferred from response headers
Expand Down Expand Up @@ -36,8 +37,7 @@

### `POST /api/v1/user/login`

**Query Params:**
- `g-recaptcha-response` (string, required) — reCAPTCHA v2/v3 token
**Query Params:** none in the verified native-client flow.

**Request Headers:**
```
Expand Down Expand Up @@ -95,7 +95,9 @@ Referer: https://portal.ut.edu.vn/
**Notes:**
- Token hết hạn sau **30 ngày** (`exp - iat = 2592000s`)
- `body` field chứa role: `"sv"` = sinh viên, có thể có `"gv"` = giảng viên
- reCAPTCHA token có thể lấy từ browser session hoặc reCAPTCHA Enterprise API
- Live native-client verification on 2026-08-11 succeeded without a cookie,
browser token, or reCAPTCHA query value. Treat browser-only challenge fields
as capability-driven rather than part of the native contract.
- Algorithm: HS256 (HMAC-SHA256)

---
Expand Down Expand Up @@ -260,6 +262,45 @@ Referer: https://portal.ut.edu.vn/

## 📅 4. Lịch học (Schedule)

### `GET /api/v1/lichhoc/ngay?date={YYYY-MM-DD}`

**Description:** Nguồn chính xác cho lịch học của một ngày. UTHelper dùng endpoint
này cho card "Lịch học hôm nay" vì response có trực tiếp môn, giờ bắt đầu/kết
thúc, tiết, phòng, cơ sở, ghi chú và trạng thái tạm ngưng.

**Authentication:** `Authorization: Bearer {jwt_token}`. Capture và live probe
đều thành công mà không cần cookie hoặc `X-Browser-Token`.

**Redacted response contract:**

```json
{
"success": true,
"status": 200,
"body": [
{
"id": 123,
"tenMonHoc": "Tên môn học",
"maLopHocPhan": "Mã lớp học phần",
"tenPhong": "Phòng học",
"coSoToDisplay": "Cơ sở",
"tuTiet": 1,
"denTiet": 3,
"tuGio": "07:00",
"denGio": "09:30",
"isTamNgung": false,
"ghiChu": ""
}
]
}
```

### `GET /api/v1/lichhoc/tuan?date={YYYY-MM-DD}`

**Description:** Lịch tuần; thêm `ngayBatDauHoc` và `giangVien` (có thể null).
Endpoint này phù hợp cho màn hình tuần, nhưng card hôm nay ưu tiên `/ngay` để
tránh lọc và suy diễn ngày ở client.

### `GET /api/v1/lichhoc/thang?date={YYYY-MM-DD}`

**Description:** Lịch học theo tháng (dùng ngày đầu tháng)
Expand Down Expand Up @@ -297,6 +338,9 @@ Referer: https://portal.ut.edu.vn/

**Params:** `date` — Ngày bất kỳ trong tuần, format `YYYY-MM-DD`

Legacy-compatible variant of the weekly contract; the verified backend also
exposes the shorter `/lichhoc/tuan` route.

### `GET /api/v1/lichhoc/songayhoc`

**Description:** Tổng số ngày đã học (attendance tracking)
Expand Down
79 changes: 79 additions & 0 deletions docs/architecture/refactoring-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -1289,3 +1289,82 @@ rg -n "Wait-Process" scripts/test_windows_single_instance_e2e.ps1
background-extension suite passes **1291 tests with 25 skipped in 27.35
seconds**; repository-wide Ruff, bytecode compilation, and `git diff --check`
pass.

## Quiz attempt badge normalization (2026-08-11)

- Moodle quiz detail responses now distinguish an authoritative empty attempt
list from an unavailable attempt endpoint. An empty list maps to `Chưa làm`;
finished, in-progress, overdue, abandoned, and unknown non-empty attempt
states receive explicit normalized progress states.
- `DataOrchestrator` promotes that normalized state to the existing activity
status field, while the dashboard badge presenter keeps assignment wording
unchanged and renders quiz-specific labels.
- A live read-only check against the reported quiz confirmed the original API
response contained zero attempts while the previous UI retained `unknown`.
Regression coverage now includes empty/error responses, all known attempt
states, data-layer promotion, and badge wording.
- Verification: 61 focused adapter/orchestrator/presenter/UI tests pass. Ruff,
bytecode compilation, and `git diff --check` pass. The full local suite has
**1293 passed and 25 skipped**; its seven failures are the same baseline
environment gaps for the optional iOS bridge and diagnostics SDK.

## Native startup frame and Portal daily schedule (2026-08-11)

- Windows now starts Flet with the native host hidden, applies the final
420×720 geometry, and publishes a branded splash with an animated progress
ring before importing and constructing the full dashboard. The prior visible
1280×720 empty Flutter frame is no longer part of the startup lifecycle.
- "Lịch học hôm nay" no longer filters Moodle quiz/assignment deadlines. A
separate Portal boundary authenticates in memory and reads the verified
`/lichhoc/ngay` contract from `portal-publicapp.ut.edu.vn`, preserving subject,
class code, start/end time, periods, room, campus, cancellation, and note.
- An account-scoped atomic cache distinguishes an authoritative empty day from
missing data. The coordinator fetches when today's cache is absent at app
open and refreshes while the app is alive at 00:00 and 06:00. The disclosure
shows explicit loading/auth/error/empty states and derives upcoming,
in-progress, finished, or cancelled status from the current local time.
- Live verification returned two current-day sessions with complete subject,
time, room, and campus data and round-tripped the redacted schedule cache.
Native Windows smoke observed 47 consecutive first-start frames at exactly
420×720 and captured the branded splash before the dashboard; a second native
smoke rendered both live sessions in the expanded card. Smoke-owned scripts,
appdata, logs, and screenshots were deleted before commit.
- The live account identifier and password prefix previously embedded as grep
markers in an archived implementation plan were replaced with synthetic
markers; no supplied credential or live room/class identifier remains in the
tracked tree.
- Verification: **1303 tests passed with 25 skipped**; `ruff check src tests`
and `git diff --check` passed.

## Windows Burn installer branding (2026-08-11)

- The WiX Bundle now uses the application ICO as `IconSourceFile`, which gives
the generated bootstrapper EXE, Explorer entry, Programs and Features entry,
and installer title bar the UTHelper identity instead of the WiX default.
- WixStdBA now uses the application PNG as `LogoFile`, replacing its generic
content logo. The application ICO was rebuilt as a transparent square,
multi-resolution resource with 16, 24, 32, 48, 64, 128, and 256 pixel frames.
- A regression contract checks both WiX bindings and the required Windows shell
icon sizes. A real local WiX 7 Bundle build completed without warnings; shell
resource extraction and a captured WixStdBA window confirmed the exterior
EXE icon, title-bar icon, and content logo.
- Verification: **1284 tests passed with 25 skipped** under the same source paths
as CI; `ruff check src tests` and `git diff --check` passed.

## v2.3.0 minor release preparation (2026-08-11)

- The released `v2.2.12` tree was compared with the integrated `develop`
branch under Semantic Versioning 2.0.0. This release adds the user-facing
Portal daily schedule with startup/00:00/06:00 refresh behavior, explicit
schedule loading states, and room/campus/session status presentation.
- The remaining changes are backward-compatible fixes: branded Windows
installer surfaces, a correctly sized branded startup frame, and normalized
quiz attempt badges. No public contract was removed or incompatibly changed.
The new compatible feature therefore advances the minor version to `2.3.0`
rather than using another patch or a major release.
- `pyproject.toml` remains the sole authored version source. Release metadata
resolves `v2.3.0` to monotonic build number `2003000`; runtime and native
package versions continue to be generated from that source.
- Verification: the complete suite passes **1321 tests with 25 skipped**;
repository-wide Ruff, bytecode compilation, release tag/build metadata
validation, and `git diff --check` pass.
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ contract.

- [ ] **Step 6: Scan for secrets/authenticated URLs and commit**

Run: `rg -n "NoBoi|080206011901|wstoken=|token=[A-Za-z0-9]{10,}" src tests docs REFAC_KNOWLEDGE.md`
Run: `rg -n "KNOWN_PASSWORD_MARKER|KNOWN_STUDENT_ID|wstoken=|token=[A-Za-z0-9]{10,}" src tests docs`

Expected: no newly introduced credential or authenticated URL. Existing generic
parameter names such as `wstoken` in source are reviewed manually and are not secret
Expand Down
7 changes: 7 additions & 0 deletions docs/guides/windows-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ machine-scoped WiX 7 MSI and a Burn bootstrapper EXE. Review and accept the WiX
7 OSMF EULA before setting the required process variable; the build refuses to
restore or execute WiX otherwise.

Burn branding is sourced from the application assets rather than duplicated in
the packaging directory. `src/assets/icon.ico` supplies the executable,
Explorer, Programs and Features, and title-bar icon through `IconSourceFile`;
`src/assets/icon.png` supplies the WixStdBA content logo through `LogoFile`.
Keep the ICO multi-resolution, including 16, 32, 48, and 256 pixel square
frames, so Windows can select a native frame at each shell DPI.

```powershell
$env:WIX_EULA_ACCEPTED = "wix7"
.\scripts\build_installer.ps1 -BundleDir build\windows -OutputDir dist
Expand Down
2 changes: 2 additions & 0 deletions packaging/windows/Bundle.wxs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
<Bundle Name="UTHelper" Manufacturer="UTHelper" Version="$(Version)"
IconSourceFile="$(BundleIconPath)"
UpgradeCode="EECFB4A5-4CCD-4D94-A0DD-D8D346F626E0">
<BootstrapperApplication>
<bal:WixStandardBootstrapperApplication
Theme="hyperlinkLicense"
LicenseUrl=""
LogoFile="$(BundleLogoPath)"
LaunchTarget="[ProgramFiles64Folder]UTHelper\UTHelper.exe"
LaunchWorkingFolder="[ProgramFiles64Folder]UTHelper" />
</BootstrapperApplication>
Expand Down
2 changes: 1 addition & 1 deletion packaging/windows/UTHelper.Bundle.wixproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<EnableDefaultItems>false</EnableDefaultItems>
<InstallerPlatform>x64</InstallerPlatform>
<OutputName>UTHelper-Setup-$(Version)</OutputName>
<DefineConstants>Version=$(Version);MsiPath=$(MsiPath)</DefineConstants>
<DefineConstants>Version=$(Version);MsiPath=$(MsiPath);BundleIconPath=$(MSBuildProjectDirectory)\..\..\src\assets\icon.ico;BundleLogoPath=$(MSBuildProjectDirectory)\..\..\src\assets\icon.png</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="Bundle.wxs" />
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "uthelper"
version = "2.2.12"
version = "2.3.0"
description = "UTHelper — Ứng dụng theo dõi bài tập và deadline UTH E-learning"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
Binary file modified src/assets/icon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ class Settings(BaseModel):
# Địa chỉ mấy trang web của trường mình
MOODLE_BASE_URL: str = "https://courses.ut.edu.vn"
MOODLE_LOGIN_URL: str = "https://courses.ut.edu.vn/login/index.php"
PORTAL_API_BASE: str = "https://portal.ut.edu.vn/api/v1"
PORTAL_API_BASE: str = "https://portal-publicapp.ut.edu.vn/api/v1"

# Cài đặt chung của ứng dụng
THEME: str = Field(default="midnight_blue", description="Theme preset: midnight_blue, ocean_teal, sakura_pink, nord_frost, monokai_pro, solarized_dark")
Expand Down
7 changes: 6 additions & 1 deletion src/core/data_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ def _fetch_detail_via_ws(self, activity_data: Dict[str, Any]) -> Optional[Dict[s
details['open_time'] = ws_details['open_time']
if ws_details.get('quiz_info'):
details['quiz_info'] = ws_details['quiz_info']
if ws_details.get('quiz_attempt_status'):
details['quiz_attempt_status'] = ws_details['quiz_attempt_status']
if ws_details.get('attempts_allowed'):
details['attempts_allowed'] = ws_details['attempts_allowed']
if ws_details.get('time_limit'):
Expand All @@ -593,7 +595,10 @@ def _fetch_detail_via_ws(self, activity_data: Dict[str, Any]) -> Optional[Dict[s

# Cập nhật submission_status ở top level nếu có
status_data = ws_details.get('status_data', {})
if 'Trạng thái nộp bài' in status_data:
quiz_attempt_status = ws_details.get('quiz_attempt_status')
if activity_type == 'quiz' and quiz_attempt_status:
result['submission_status'] = quiz_attempt_status
elif 'Trạng thái nộp bài' in status_data:
result['submission_status'] = status_data['Trạng thái nộp bài']
elif 'Trạng thái' in status_data:
result['submission_status'] = status_data['Trạng thái']
Expand Down
Loading
Loading