diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 00000000..02be1cdc
--- /dev/null
+++ b/.claude/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.0.1",
+ "configurations": [
+ {
+ "name": "web-ui",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": ["run", "api"],
+ "port": 3010
+ }
+ ]
+}
diff --git a/.gitignore b/.gitignore
index 356fb010..aa2cfb2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,5 @@ note
.DS_Store
.playwright-chromium-installed
.stfolder/
-.env
\ No newline at end of file
+.env
+scheduled_tasks.json
\ No newline at end of file
diff --git a/PROXY_GUIDE.vi.md b/PROXY_GUIDE.vi.md
new file mode 100644
index 00000000..f968f439
--- /dev/null
+++ b/PROXY_GUIDE.vi.md
@@ -0,0 +1,336 @@
+# Hướng dẫn cài đặt Proxy — Microsoft Rewards Script
+
+Tài liệu này dành cho người **chưa từng dùng proxy bao giờ**. Đọc từ trên xuống, làm đúng thứ tự là được.
+
+---
+
+## 1. Proxy là gì? (giải thích bằng ví dụ đời thường)
+
+Hãy tưởng tượng bạn muốn gửi một lá thư nhưng **không muốn người nhận biết địa chỉ nhà bạn**.
+
+- **Không có proxy:** bạn tự đi bộ đến nhà người nhận, họ thấy mặt bạn và biết bạn ở đâu.
+- **Có proxy:** bạn đưa lá thư cho một người trung gian. Người đó đi giao thư. Người nhận chỉ thấy **địa chỉ của người trung gian**, không thấy bạn.
+
+**Proxy chính là "người trung gian" đó.** Nó là một máy chủ trung gian nằm giữa máy tính của bạn và trang web Microsoft, giúp:
+
+1. **Giấu địa chỉ IP thật của bạn** — Microsoft chỉ thấy IP của proxy.
+2. **Tạo cảm giác mỗi tài khoản đến từ một nơi khác nhau** — thay vì 4 tài khoản cùng phát ra từ một IP nhà bạn.
+
+### Vì sao điều này quan trọng với tool này?
+
+Nếu bạn chạy 4 tài khoản Microsoft Rewards **từ cùng một IP nhà**, Microsoft thấy 4 tài khoản hoạt động chung một chỗ, cùng giờ, cùng kiểu — đó là dấu hiệu rất rõ của bot. **Proxy giúp mỗi tài khoản trông như một người riêng biệt ngồi ở một thành phố riêng.**
+
+> ⚠️ **Nói thẳng:** proxy **giảm** nguy cơ bị khóa tài khoản, nhưng **không loại bỏ hoàn toàn**. Microsoft vẫn có thể phát hiện bot bằng nhiều cách khác. Đừng dùng proxy rồi nghĩ mình an toàn tuyệt đối.
+
+---
+
+## 2. Bạn có thật sự cần proxy không?
+
+| Tình huống | Có cần proxy? |
+| ---------------------------------------- | ------------------------------------------- |
+| Chỉ chạy **1 tài khoản** | ❌ Không cần. IP nhà bạn là đủ tự nhiên. |
+| Chạy **2–3 tài khoản**, chấp nhận rủi ro | ⚠️ Nên có, nhưng chưa bắt buộc |
+| Chạy **4 tài khoản trở lên** | ✅ **Rất nên có.** Mỗi tài khoản một proxy. |
+| Tài khoản đã từng bị khóa/cảnh báo | ✅ Nên có, kèm proxy **dân cư** (xem mục 3) |
+
+**Nếu bạn chỉ có 1 tài khoản, hãy bỏ qua toàn bộ tài liệu này** — thêm proxy vào chỉ làm tool chạy chậm hơn và dễ lỗi hơn mà không được gì.
+
+---
+
+## 3. Chọn loại proxy nào?
+
+Tool này hỗ trợ 4 loại, nhưng **thực tế bạn chỉ nên dùng 2 loại đầu**:
+
+| Loại | Ghi trong ô "Proxy address" | Dùng được mật khẩu? | Nên dùng? |
+| --------- | --------------------------- | ------------------- | ----------------------------------- |
+| **HTTP** | `http://...` | ✅ Có | ✅ **Phổ biến nhất, chọn cái này** |
+| **HTTPS** | `https://...` | ✅ Có | ✅ Tốt, mã hóa đường truyền |
+| SOCKS4 | `socks4://...` | ❌ **Không** | ⚠ Chỉ khi proxy không cần đăng nhập |
+| SOCKS5 | `socks5://...` | ❌ **Không** | ⚠ Chỉ khi proxy không cần đăng nhập |
+
+### Vì sao SOCKS không dùng được mật khẩu?
+
+Tool điều khiển trình duyệt bằng thư viện tên **Patchright**, và Patchright **không hỗ trợ proxy SOCKS có xác thực**. Nếu bạn nhập tài khoản/mật khẩu cho proxy SOCKS, Web UI sẽ **báo lỗi ngay** và không lưu:
+
+```
+SOCKS5 proxy authentication is not supported by Patchright
+```
+
+→ **Cách xử lý:** hoặc dùng proxy HTTP/HTTPS (có mật khẩu), hoặc dùng proxy SOCKS **không cần** đăng nhập. Đừng cố nhập mật khẩu cho SOCKS.
+
+### Dân cư (residential) hay Trung tâm dữ liệu (datacenter)?
+
+- **Datacenter** (giá rẻ, ví dụ $1–3/tháng): IP thuộc về trung tâm dữ liệu. Microsoft **biết rõ** dải IP này. Vẫn dùng được nhưng tỉ lệ bị nghi ngờ cao hơn.
+- **Residential** (đắt hơn, $3–15/tháng): IP thật của hộ gia đình. Tự nhiên nhất, khó bị phát hiện nhất. **Đây là loại nên dùng nếu bạn nghiêm túc.**
+
+### Mua ở đâu?
+
+Bạn cần tự mua — tool không kèm proxy. Một số nhà cung cấp phổ biến:
+
+- Webshare, IPRoyal, Smartproxy, Oxylabs, Bright Data
+
+**Khi mua, bạn sẽ nhận được 4 thông tin** — hãy ghi lại đủ 4:
+
+```
+Địa chỉ (host): proxy.provider.com ← thứ này vào ô "Proxy address"
+Cổng (port): 8080 ← vào ô "Port"
+Tên đăng nhập: myuser123 ← vào ô "Username"
+Mật khẩu: aBcD1234xyz ← vào ô "Password"
+```
+
+> 💡 **Mẹo tiết kiệm:** mua gói **nhiều proxy cùng lúc** (ví dụ 5 proxy). Thường nhà cung cấp bán theo gói, mua lẻ 1 cái thường đắt hơn nhiều.
+
+### ⚠️ Phân biệt nơi BÁN proxy riêng và nơi cho DANH SÁCH proxy công cộng
+
+Đây là chỗ rất nhiều người mua nhầm. Có **hai loại website hoàn toàn khác nhau**:
+
+| Loại website | Bạn nhận được gì | Dùng cho tool này? |
+| ----------------------------------------------- | ---------------------------------------------------- | ----------------------- |
+| **Nhà bán proxy riêng** (Webshare, IPRoyal...) | 1 proxy **riêng của bạn**, có tài khoản/mật khẩu | ✅ **Đúng thứ bạn cần** |
+| **Danh sách proxy công cộng** (free proxy list) | Hàng nghìn địa chỉ IP dùng chung với **cả thế giới** | ❌ **Đừng dùng** |
+
+**Vì sao danh sách proxy công cộng không dùng được?**
+
+1. **Ai cũng dùng được** — hàng nghìn người khác cũng đang dùng đúng cái IP đó. Microsoft thấy một IP có 500 người đăng nhập khác nhau thì đó là dấu hiệu xấu, không phải tốt.
+2. **Tốc độ rất chậm và hay chết** — proxy công cộng thường chết sau vài giờ, thậm chí vài phút. Bạn sẽ phải thay liên tục.
+3. **Không có tài khoản/mật khẩu** — nghĩa là bạn không kiểm soát được gì, và không biết ai đang đọc dữ liệu đi qua đó.
+4. **Nhiều cái là mồi** — một số trang cố tình đăng proxy để thu thập dữ liệu người dùng. **Tuyệt đối không đăng nhập tài khoản Microsoft qua proxy công cộng không rõ nguồn.**
+
+> 🚨 **Dấu hiệu nhận biết:** nếu trang web hiển thị một **bảng dài hàng nghìn dòng IP:port** và cho bạn bấm "Copy" miễn phí mà **không hỏi mật khẩu đăng nhập proxy** — đó là danh sách công cộng. Đừng dùng cho tài khoản Microsoft.
+>
+> Ngược lại, trang **bán** proxy sẽ cho bạn **một** địa chỉ duy nhất (hoặc một gói vài cái), kèm **username và password riêng của bạn**.
+
+---
+
+## 4. Cách nhập proxy vào Web UI (từng bước)
+
+**Điều kiện:** Web UI đang chạy (xem `WEB_UI_GUIDE.vi.md` mục 3). Tool **KHÔNG được chạy** khi bạn đang sửa proxy.
+
+### Bước 1 — Mở trình soạn proxy
+
+Trong danh sách **Ready to Execute**, tìm dòng tài khoản bạn muốn gán proxy. Bấm nút **Proxy** trên dòng đó.
+
+Khung **"Proxy for [email tài khoản]"** sẽ hiện ra ngay bên dưới. Tên tài khoản hiện ở tiêu đề để bạn chắc chắn mình đang sửa đúng người.
+
+### Bước 2 — Điền thông tin
+
+| Trường | Điền gì | Ví dụ |
+| ----------------- | -------------------------------- | --------------------------- |
+| **Proxy address** | Địa chỉ proxy, **kèm `http://`** | `http://proxy.provider.com` |
+| **Port** | Cổng, số từ 1 đến 65535 | `8080` |
+| **Username** | Tên đăng nhập proxy (nếu có) | `myuser123` |
+| **Password** | Mật khẩu proxy (nếu có) | `aBcD1234xyz` |
+
+**Ba lưu ý quan trọng:**
+
+1. **Phải có `http://` ở đầu.** Gõ `proxy.provider.com` không có `http://` thì tool tự hiểu là `http://` (vẫn chạy được), nhưng cứ gõ đầy đủ cho chắc.
+2. **KHÔNG nhập tài khoản/mật khẩu vào ô Proxy address.** Viết `http://user:pass@proxy.com` là **sai** — Web UI sẽ báo lỗi. Tài khoản và mật khẩu phải nằm ở 2 ô riêng.
+3. **Username và Password phải điền cùng nhau.** Có tên mà không có mật khẩu (hoặc ngược lại) sẽ bị từ chối.
+
+### Bước 3 — Chọn công tắc "Use for API requests too"
+
+Công tắc này quyết định proxy **che những gì**. Đọc kỹ mục 5 bên dưới rồi mới bật.
+
+### Bước 4 — Bấm **Save Proxy**
+
+- Thấy thông báo xanh (toast) **"Proxy saved"** = thành công.
+- Dòng tài khoản sẽ hiện **huy hiệu xanh `host:port`** bên cạnh email → bạn biết tài khoản này đã có proxy.
+- Thay đổi **chỉ áp dụng cho lần chạy tới**. Nếu tool đang chạy, bấm **Stop** rồi chạy lại.
+
+### Xóa proxy (khi không cần nữa)
+
+Mở lại khung, bấm **Remove Proxy** → xác nhận. Proxy của tài khoản đó bị xóa sạch.
+
+Hoặc: xóa trắng ô **Proxy address** rồi bấm **Save Proxy** — kết quả giống hệt.
+
+---
+
+## 5. Công tắc "Use for API requests too" — quan trọng nhất
+
+Đây là chỗ **hầu hết người mới hiểu sai**. Tool này liên lạc với Microsoft qua **2 đường riêng biệt**:
+
+```
+ ┌─────────────────────────────────────┐
+ │ Máy tính của bạn │
+ └──────────┬──────────────┬───────────┘
+ │ │
+ (1) Trình duyệt (2) Gọi API trực tiếp
+ (Edge) (tính điểm, nhiệm vụ)
+ │ │
+ ▼ ▼
+ ┌─────────┐ ┌─────────┐
+ │ PROXY? │ │ PROXY? │ ← công tắc quyết định ở đây
+ └────┬────┘ └────┬────┘
+ │ │
+ └──────┬───────┘
+ ▼
+ Microsoft Rewards
+```
+
+| Công tắc | Trình duyệt (Edge) | Gọi API trực tiếp |
+| ------------------ | ------------------ | ------------------------- |
+| **TẮT** (mặc định) | ✅ Đi qua proxy | ❌ Đi thẳng từ IP nhà bạn |
+| **BẬT** | ✅ Đi qua proxy | ✅ Đi qua proxy |
+
+### Vậy nên chọn cái nào?
+
+**Khuyên dùng: TẮT** (để nguyên mặc định).
+
+**Lý do:** Nhiều proxy (nhất là loại rẻ) **chặn hoặc làm hỏng** các request API. Khi bật công tắc này, tool hay gặp lỗi kiểu:
+
+```
+Failed to fetch dashboard data
+Request timeout
+403 Forbidden
+```
+
+Nếu bạn đang gặp mấy lỗi này sau khi bật proxy → **tắt công tắc đi và chạy lại**. Đây là nguyên nhân số 1.
+
+**Khi nào nên BẬT?**
+
+- Bạn dùng proxy **residential loại tốt**, đã kiểm tra chạy ổn với API.
+- Bạn chạy **nhiều tài khoản** và muốn **che tuyệt đối** — không muốn bất kỳ request nào lộ IP nhà.
+- Bạn đã bị Microsoft cảnh báo và muốn che chắn tối đa.
+
+**Quy tắc thực tế:** Chạy thử với công tắc **TẮT** trước. Nếu mọi thứ suôn sẻ và bạn muốn an toàn hơn, hãy thử **BẬT** rồi quan sát log. Gặp lỗi thì tắt lại.
+
+---
+
+## 6. Nguyên tắc vàng: MỘT tài khoản = MỘT proxy
+
+Đây là sai lầm phổ biến nhất và cũng là sai lầm **nguy hiểm nhất**.
+
+| Cách làm | Kết quả |
+| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 4 tài khoản dùng **4 proxy khác nhau** | ✅ Đúng. Mỗi tài khoản như một người riêng. |
+| 4 tài khoản dùng **chung 1 proxy** | ❌ **Vô nghĩa.** Microsoft vẫn thấy 4 tài khoản cùng một IP — y như không dùng proxy. Thậm chí tệ hơn, vì giờ nó là IP của proxy (dễ bị gắn cờ). |
+| 3 tài khoản dùng proxy, 1 tài khoản không | ⚠ Tài khoản không proxy vẫn lộ IP nhà. Chấp nhận được nhưng không tối ưu. |
+
+**Nhớ kỹ:** Proxy chỉ có tác dụng khi **mỗi tài khoản có một IP riêng**. Dùng chung thì công cốc.
+
+> 💡 Nếu ngân sách hạn chế: **giảm số tài khoản** xuống bằng số proxy bạn có, thay vì dùng chung proxy. Chạy 3 tài khoản tử tế an toàn hơn 6 tài khoản dùng chung 2 proxy.
+
+---
+
+## 7. Kiểm tra proxy có hoạt động không
+
+Có proxy "trông đúng" nhưng thực tế không chạy. Cách kiểm tra **trước khi** chạy tool:
+
+### Cách 1 — Kiểm tra bằng trình duyệt (dễ nhất, khuyên dùng)
+
+1. Mở **Edge** hoặc **Chrome**.
+2. Vào trang: `https://whatismyipaddress.com`
+3. Xem **IP hiện tại của bạn** và ghi nhớ nó (ví dụ `113.161.x.x`).
+4. Cài extension proxy (ví dụ _Proxy SwitchyOmega_) → nhập đúng thông tin proxy của bạn → bật lên.
+5. Tải lại trang `whatismyipaddress.com`.
+6. **IP phải ĐỔI thành IP khác.** Nếu vẫn là IP cũ → proxy không hoạt động, đừng nhập vào tool.
+
+### Cách 2 — Kiểm tra bằng dòng lệnh
+
+Mở Terminal trong thư mục tool và chạy (thay thông tin proxy của bạn vào):
+
+```bash
+curl -x http://myuser123:aBcD1234xyz@proxy.provider.com:8080 https://api.ipify.org
+```
+
+- In ra một địa chỉ IP → ✅ proxy chạy tốt.
+- Báo lỗi `Could not resolve host`, `Connection refused`, `407 Proxy Authentication Required` → proxy có vấn đề. Xem mục 8.
+
+> ⚠ Trong lệnh `curl` ở trên, tài khoản/mật khẩu **nằm trong cùng chuỗi** với `@`. Đó là **cú pháp của riêng `curl`**, KHÔNG phải cách nhập vào Web UI. Trong Web UI, bạn luôn tách ra 2 ô riêng.
+
+---
+
+## 8. Lỗi thường gặp & cách sửa
+
+### ❌ "Proxy URL is required when other proxy settings are configured"
+
+Bạn điền Port/Username/Password nhưng **để trống Proxy address**. Hoặc ngược lại, muốn xóa proxy nhưng chỉ xóa mỗi địa chỉ.
+**Sửa:** điền đủ địa chỉ, hoặc bấm **Remove Proxy** để xóa sạch mọi thứ.
+
+### ❌ "Proxy port must be an integer from 1 to 65535"
+
+Ô Port trống, hoặc bạn gõ chữ, hoặc gõ số ngoài khoảng (ví dụ `70000`, `0`).
+**Sửa:** nhập số từ `1` đến `65535`. Cổng phổ biến: `8080`, `3128`, `1080`.
+
+### ❌ "Put proxy credentials in the username/password fields, not in the proxy URL"
+
+Bạn gõ `http://user:pass@proxy.com` vào ô Proxy address.
+**Sửa:** ô Proxy address chỉ ghi `http://proxy.com`. Tài khoản → ô Username. Mật khẩu → ô Password.
+
+### ❌ "Proxy username and password must be configured together"
+
+Bạn điền Username mà quên Password (hoặc ngược lại).
+**Sửa:** điền cả hai, hoặc xóa trắng cả hai.
+
+### ❌ "SOCKS5 proxy authentication is not supported by Patchright"
+
+Bạn nhập tài khoản/mật khẩu cho proxy loại SOCKS.
+**Sửa:** đổi sang proxy `http://` / `https://`, hoặc xóa Username + Password nếu proxy SOCKS của bạn không cần đăng nhập.
+
+### ❌ "Unsupported proxy protocol"
+
+Bạn gõ sai tiền tố, ví dụ `ftp://` hoặc `socks://` (thiếu số).
+**Sửa:** chỉ dùng đúng 4 tiền tố: `http://`, `https://`, `socks4://`, `socks5://`.
+
+### ❌ "Cannot change a proxy while a bot run is active"
+
+Tool đang chạy thì không cho sửa proxy — vì sửa giữa chừng sẽ làm hỏng phiên đang chạy.
+**Sửa:** bấm **Stop**, đợi tool dừng hẳn, rồi sửa.
+
+### ❌ Lưu thành công nhưng chạy tool vẫn lỗi mạng
+
+Nguyên nhân theo thứ tự khả năng:
+
+1. **Proxy chết.** Kiểm tra bằng mục 7. Proxy miễn phí/rẻ thường chết liên tục.
+2. **Công tắc "Use for API requests too" đang BẬT** và proxy chặn API. → Tắt đi.
+3. **Proxy đã hết hạn dung lượng.** Nhiều gói bán theo GB; hết GB là proxy ngừng chạy dù thông tin vẫn đúng. → Vào trang nhà cung cấp kiểm tra.
+4. **Chưa chạy lại tool.** Thay đổi chỉ áp dụng cho **lần chạy tới**.
+
+### ❌ Tool chạy chậm hẳn sau khi thêm proxy
+
+Bình thường. Proxy ở nước ngoài làm mọi request chậm hơn.
+**Sửa:** chọn proxy **gần Việt Nam** (Singapore, Hong Kong) thay vì Mỹ/Âu.
+
+---
+
+## 9. Cấu hình đề xuất cho người mới
+
+Nếu bạn có **4 tài khoản**, đây là cấu hình an toàn và thực tế nhất:
+
+| Tài khoản | Proxy | "Use for API requests too" |
+| ----------- | --------------------- | -------------------------- |
+| Tài khoản 1 | Proxy riêng #1 (http) | TẮT |
+| Tài khoản 2 | Proxy riêng #2 (http) | TẮT |
+| Tài khoản 3 | Proxy riêng #3 (http) | TẮT |
+| Tài khoản 4 | Proxy riêng #4 (http) | TẮT |
+
+**Lý do chọn cấu hình này:**
+
+- **HTTP thay vì SOCKS** → dùng được mật khẩu, không vướng giới hạn của Patchright.
+- **Công tắc TẮT** → tránh được nhóm lỗi API phổ biến nhất, vẫn che được trình duyệt (phần Microsoft soi kỹ nhất).
+- **Proxy riêng từng tài khoản** → mỗi tài khoản một IP, đúng nguyên tắc vàng ở mục 6.
+
+Chạy thử vài ngày. Nếu ổn định và bạn muốn che chắn thêm, hãy thử bật công tắc cho **một** tài khoản trước, quan sát log — đừng bật hết cùng lúc, vì nếu có lỗi bạn sẽ không biết tại proxy nào.
+
+---
+
+## 10. Bảng tra nhanh
+
+| Việc cần làm | Ở đâu |
+| ---------------------------- | ------------------------------------------------------ |
+| Mở khung sửa proxy | Nút **Proxy** trên dòng tài khoản |
+| Nhập proxy | Ô **Proxy address** — nhớ có `http://` |
+| Nhập cổng | Ô **Port** — số 1–65535 |
+| Nhập tài khoản proxy | Ô **Username** |
+| Nhập mật khẩu proxy | Ô **Password** |
+| Cho proxy che cả API | Công tắc **Use for API requests too** (khuyên **TẮT**) |
+| Lưu | Nút **Save Proxy** |
+| Xóa proxy | Nút **Remove Proxy** |
+| Kiểm tra proxy sống hay chết | `https://whatismyipaddress.com` (mục 7) |
+| Proxy lưu ở đâu | File `.env`, dòng `ACCOUNT_N_PROXY_*` |
+
+> 🔒 **Lưu bảo mật:** mật khẩu proxy được lưu trong file `.env` trên máy bạn. Web UI **không bao giờ hiển thị lại** mật khẩu đã lưu — đó là lý do ô Password luôn trống khi bạn mở lại khung sửa. Muốn giữ nguyên mật khẩu cũ, **cứ để trống ô đó rồi bấm Save** — tool sẽ giữ lại mật khẩu đang có.
+
+---
+
+**Xem thêm:** `WEB_UI_GUIDE.vi.md` — hướng dẫn đầy đủ về Web UI dành cho người mới bắt đầu.
diff --git a/WEB_UI_GUIDE.md b/WEB_UI_GUIDE.md
new file mode 100644
index 00000000..38d636bc
--- /dev/null
+++ b/WEB_UI_GUIDE.md
@@ -0,0 +1,637 @@
+# Microsoft Rewards Script - Complete Beginner's Guide
+
+A start-to-finish guide to installing and using this tool through its new **Web UI**. No coding experience needed. Every command is something you copy, paste, and press Enter on.
+
+---
+
+## Table of Contents
+
+1. [What this tool actually does](#1-what-this-tool-actually-does)
+2. [Prerequisites and system requirements](#2-prerequisites-and-system-requirements)
+3. [Environment setup (one time only)](#3-environment-setup-one-time-only)
+4. [Starting the Web UI](#4-starting-the-web-ui)
+5. [How to use the Web UI](#5-how-to-use-the-web-ui)
+6. [Optional: turning on scheduling](#6-optional-turning-on-scheduling)
+7. [Troubleshooting](#7-troubleshooting)
+8. [Safety and privacy notes](#8-safety-and-privacy-notes)
+
+---
+
+## 1. What this tool actually does
+
+Think of it as **a robot that uses a web browser for you**.
+
+You normally earn Microsoft Rewards points by searching on Bing, clicking daily
+cards, and reading articles. This tool opens a real browser, signs in as you,
+and does that clicking and searching itself.
+
+There are three pieces. It helps to picture them like a restaurant:
+
+| Piece | Restaurant analogy | What it really is |
+| -------------- | ------------------ | ------------------------------------------------------- |
+| **The bot** | The chef | The part that opens browsers and collects points |
+| **The server** | The waiter | A small program that takes your orders and reports back |
+| **The Web UI** | The menu | The page you click on in your browser |
+
+You will mostly only touch **the menu** (the Web UI). But the waiter has to be
+awake for the menu to work, which is why you will start the server in Step 4 and
+leave it running.
+
+---
+
+## 2. Prerequisites and system requirements
+
+### What your computer needs
+
+- **Windows, macOS, or Linux.** Any of them is fine.
+- **About 2 GB of free disk space.** The tool downloads its own private copy of
+ a browser, which is most of that.
+- **An internet connection.**
+- **A screen.** For your very first sign-in you need to see a browser window, so
+ this doesn't work on a screen-less server for that one step.
+
+### What you need to have ready
+
+- **A Microsoft account** that already has Rewards enabled. Sign in at
+ [rewards.bing.com](https://rewards.bing.com) once in your normal browser and
+ make sure it works before automating it.
+- **Your account's email address.**
+- **Your password** _only if_ your account signs in with a password. If you use
+ the Microsoft Authenticator app with no password, you can skip it.
+- **20 to 30 minutes** for the one-time setup. Most of that is waiting for
+ downloads.
+
+### Software you will install
+
+Just one thing: **Node.js**, version 24 or newer. Step 3 walks you through it.
+
+Node.js is the engine this tool runs on. Nothing about it requires you to write
+code, the same way driving a car doesn't require you to build an engine.
+
+> **A note on comfort level:** you will be typing commands into a black window
+> (the "terminal"). That is normal and expected. You are not programming. You are
+> giving short instructions, like typing a web address into a browser.
+
+---
+
+## 3. Environment setup (one time only)
+
+Do these five steps in order. Each one builds on the last.
+
+### Step 3.1 - Install Node.js
+
+**On Windows:**
+
+1. Go to **[nodejs.org](https://nodejs.org)**.
+2. Download the **LTS** version (the big green button). Make sure the version
+ number starts with **24** or higher.
+3. Open the downloaded file and click **Next** through the installer. The default
+ options are correct, do not change anything.
+4. When it finishes, restart your computer. This matters. Windows needs a restart
+ to notice that Node.js exists.
+
+**On macOS:** download the LTS `.pkg` installer from
+[nodejs.org](https://nodejs.org) and run it.
+
+**On Linux:** use [nodesource](https://github.com/nodesource/distributions) or
+your package manager, and confirm you get version 24 or newer.
+
+**Check that it worked.** Open a terminal:
+
+- **Windows:** press the **Windows key**, type `powershell`, press Enter.
+- **macOS:** press **Cmd+Space**, type `terminal`, press Enter.
+- **Linux:** press **Ctrl+Alt+T**.
+
+Type this and press Enter:
+
+```bash
+node --version
+```
+
+You should see something like `v24.5.0`. If you see "command not found", Node.js
+did not install correctly, or you skipped the restart.
+
+> **If the number is lower than 24**, you have an old Node.js. Install the new
+> one over it using the steps above.
+
+### Step 3.2 - Get the tool onto your computer
+
+**The easy way (no Git needed):**
+
+1. Go to the project's GitHub page.
+2. Click the green **Code** button, then **Download ZIP**.
+3. Right-click the downloaded ZIP and choose **Extract All**.
+4. Move the extracted folder somewhere you'll remember, like your Documents
+ folder.
+
+> **Avoid folders synced by OneDrive, Dropbox, or Google Drive if you can.**
+> They sometimes lock files while the tool is writing to them, which causes
+> confusing errors.
+
+### Step 3.3 - Point your terminal at the folder
+
+Your terminal needs to be "inside" the project folder, the way File Explorer is
+inside a folder when you double-click it.
+
+**The easiest method on Windows:**
+
+1. Open the project folder in File Explorer.
+2. Click the address bar at the top (where the folder path is shown).
+3. Type `powershell` and press Enter.
+
+A terminal opens already pointed at the right place.
+
+**Any platform:** type `cd ` (with a space), then drag the folder from your file
+manager onto the terminal window, then press Enter.
+
+**Check that you're in the right place:**
+
+```bash
+ls
+```
+
+You should see file names including `package.json` and `config.example.json`. If
+you don't, you're in the wrong folder.
+
+> **Keep this terminal open.** Every command from here on gets typed into it.
+
+### Step 3.4 - Install the tool's parts
+
+Type this and press Enter:
+
+```bash
+npm run pre-build
+```
+
+**This takes a while.** Five to fifteen minutes is normal. It is downloading the
+tool's private browser, which is a few hundred megabytes.
+
+You will see a lot of scrolling text. That's fine. Warnings in yellow are
+normal and safe to ignore. Only stop and worry if it ends with the word `error`
+and nothing else happens.
+
+Then run:
+
+```bash
+npm run build
+```
+
+This one is quick, usually under a minute. It converts the project's source
+files into the form the computer actually runs.
+
+> **Remember this command.** Any time you change the project's files, you run
+> `npm run build` again to apply the change.
+
+### Step 3.5 - Create your two settings files
+
+The project ships **example** settings files. You make your own copies and fill
+them in. The examples are templates, like a blank form.
+
+**File 1: `config.json`** - controls how the bot behaves.
+
+Find the file named `config.example.json` in the project folder. **Copy** it, and
+rename the copy to exactly `config.json`.
+
+- Windows: right-click → Copy, right-click → Paste, then rename
+ `config.example - Copy.json` to `config.json`.
+- Make sure the name is exactly `config.json`, with no "example" and no "copy".
+
+You do not need to edit anything inside it. The defaults are fine.
+
+**File 2: `.env`** - holds your account email and password.
+
+Find the file named `env.example`. Copy it and rename the copy to exactly
+`.env` - starting with a dot, and with **no** file extension.
+
+> **Windows tip:** Explorer may refuse to let you name a file starting with a
+> dot. Work around it by naming it `.env.` **with a trailing dot** - Windows
+> strips that automatically and leaves you with `.env`.
+
+Now open `.env` in **Notepad** (right-click → Open with → Notepad). You'll see
+lines like this:
+
+```env
+ACCOUNT_1_EMAIL=email@example.com
+#ACCOUNT_1_PASSWORD=your_password
+```
+
+Change the email to your real one. If your account uses a password, delete the
+`#` at the start of the password line and put your real password in:
+
+```env
+ACCOUNT_1_EMAIL=your.real.email@outlook.com
+ACCOUNT_1_PASSWORD=YourRealPassword
+```
+
+**The `#` means "ignore this line."** Removing it switches the line on.
+
+If your account uses the Authenticator app instead of a password, leave the
+password line alone with its `#`. The tool handles that.
+
+Save the file and close Notepad.
+
+> **If you use two-factor authentication**, there's a line for
+> `ACCOUNT_1_TOTP_SECRET`. Filling it in lets the tool generate your 6-digit
+> codes automatically. To get the value: in your Microsoft security settings,
+> open "Manage how you sign in", add an authenticator app, and when the QR code
+> appears choose **"enter code manually"**. Paste that code as the value.
+
+**Setup is done.** You never have to repeat Steps 3.1 through 3.5.
+
+---
+
+## 4. Starting the Web UI
+
+Two steps: wake up the server, then open the page.
+
+### Step 4.1 - Start the server
+
+In your terminal (still inside the project folder), run:
+
+```bash
+npm run api
+```
+
+You'll see a few lines of startup text and then the terminal will appear to
+**hang**, showing no new output and no prompt.
+
+**That is correct.** The server is now running and listening. It's supposed to
+sit there. The terminal is not frozen.
+
+> **Leave this terminal window open and do not close it.** Closing it turns the
+> server off, and the Web UI stops working. Minimize it instead.
+
+### Step 4.2 - Open the page
+
+Open your normal web browser (Chrome, Edge, Firefox, whatever you use) and go to:
+
+```
+http://127.0.0.1:3010
+```
+
+`127.0.0.1` always means "this computer." You're not visiting the internet, just
+your own machine.
+
+You should see the **Microsoft Rewards Control** dashboard.
+
+Look at the top right. There's a **Server** badge:
+
+| Badge | Meaning |
+| ----------- | ----------------------------------------------------------- |
+| **Online** | Server is awake, nothing running. This is what you want. |
+| **Running** | A run is in progress right now. |
+| **Offline** | The page can't reach the server. Check Step 4.1's terminal. |
+
+If it says Offline, your server terminal is probably closed or errored out.
+
+### Every time after today
+
+Starting the tool is just those two steps:
+
+1. Open a terminal in the project folder, run `npm run api`
+2. Open `http://127.0.0.1:3010`
+
+---
+
+## 5. How to use the Web UI
+
+The page is a single column of sections, top to bottom. Here's each one.
+
+### 5.1 - Add Account
+
+The box at the top. Type an email address and click **Add Account**.
+
+This writes the email into your `.env` file for you, so you don't have to edit
+that file by hand again. The account appears in the list immediately.
+
+> **Important: the UI never asks for your password, on purpose.** Passwords only
+> ever live in your `.env` file on your own disk. If the account you just added
+> needs a password, open `.env` in Notepad and add a matching line. For an
+> account added as `ACCOUNT_2`, the line is `ACCOUNT_2_PASSWORD=yourpassword`.
+
+### 5.2 - Account Overview
+
+Four counters showing the health of your accounts.
+
+| Counter | What it means |
+| ------------------ | --------------------------------------------------------------- |
+| **Total Accounts** | How many accounts the tool knows about |
+| **Logged In** | Has a valid saved sign-in. Ready to run. |
+| **Not Logged In** | Has never signed in. Needs the step below. |
+| **Login Expired** | Was signed in, but the saved sign-in went stale. Sign in again. |
+
+**A brand new account starts as "Not Logged In." That's expected.**
+
+### 5.3 - First sign-in for a new account
+
+Microsoft will not let a robot get through a fresh sign-in cleanly, so you do
+the first one yourself. Once. It gets saved after that.
+
+Open a **second** terminal in the project folder (leave the server one alone)
+and run this, with your own email:
+
+```bash
+npm run manual-login -- --email your.real.email@outlook.com
+```
+
+A browser window opens. Sign in normally, exactly like any other day, including
+any 2FA prompt. When you land on the Rewards page, **wait about five seconds**.
+The window closes by itself and your sign-in is saved.
+
+Back on the dashboard, that account flips to **Logged In** within a few seconds.
+
+> Do this again whenever an account shows **Login Expired**. It's the standard
+> fix for almost every sign-in problem.
+
+### 5.4 - Execution Settings
+
+Four controls that decide _how_ the next run behaves. **Set these before you
+press Run.** Your choices are remembered in your browser for next time.
+
+**Run in Headless Mode**
+
+- **Off** (default): you see the browser windows open and click around.
+- **On**: everything happens invisibly in the background.
+
+Off is better while you're learning, because you can see what's happening. On is
+better once you trust it, so windows don't steal focus while you work.
+
+> Don't use headless for a first sign-in. Use `manual-login` from Section 5.3.
+
+**Run Visual Search**
+
+Off by default. Turn it on to also do Bing Visual Search tasks (the ones where
+you search using a picture instead of words) for extra daily points.
+
+**Run 30-Minute Edge Browsing**
+
+Off by default. Turn it on to complete the Edge browsing reward, where the tool
+browses in the background for half an hour.
+
+> **This makes the run take at least 30 minutes longer.** That's the reward's
+> own requirement, not the tool being slow. It's marked experimental because
+> Microsoft changes how it works from time to time.
+
+**Schedule for Later**
+
+A date and time picker. Leave it **empty** to run right now. Fill it in only if
+you're using the Schedule Run button (see Section 5.7).
+
+### 5.5 - Ready to Execute
+
+Your account list, plus the action buttons.
+
+Each account row shows a **checkbox**, the **email**, how many **runs** it has
+done, when it **last** ran, its **points**, and a **status badge**.
+
+**How to run:**
+
+1. **Tick the checkbox** on each account you want to run. Or click **Select All**
+ to tick everything (it turns into **Deselect All**).
+2. Double-check your toggles in Execution Settings.
+3. Click **Run Selected**.
+
+A small message slides in confirming the start, the Server badge flips to
+**Running**, and log lines start appearing at the bottom of the page.
+
+**What to expect:** a full run takes a while, commonly 20 to 60 minutes per
+account, longer with Edge Browsing on. The tool deliberately pauses between
+actions so it behaves like a person rather than a machine. Slow is intentional.
+
+**The Stop button** is greyed out unless something is actually running. Click it
+to end the run: it asks the bot to shut its browsers down cleanly, and if the bot
+doesn't respond in time the server force-closes it. No orphaned browser windows
+left behind either way.
+
+**The Remove button** takes an account off this list, but it **does not delete it
+from your `.env` file**, so it will reappear on the next refresh. To remove an
+account for good, open `.env` in Notepad and delete its `ACCOUNT_N_EMAIL` line.
+
+### 5.6 - Per-account proxy
+
+Every account row has a **Proxy** button. Click it to open the proxy editor just
+below the account list.
+
+The editor takes a **Proxy address**, **Port**, **Username**, **Password**, and a
+**Use for API requests too** toggle. After saving, the account row shows a small
+blue `host:port` badge so you can see at a glance which accounts have a proxy.
+
+**You only need this section if you run more than one account.** With a single
+account, skip it — adding a proxy just makes the tool slower.
+
+> 📖 **What a proxy is, which kind to buy, where to get one, and how to fix the
+> common errors** — see the dedicated guide: **`PROXY_GUIDE.vi.md`** (Vietnamese).
+> It is written for someone who has never used a proxy before.
+
+**Four things worth knowing right away:**
+
+- **One account, one proxy.** Several accounts sharing a single proxy defeats the
+ purpose entirely — Microsoft still sees them all on the same IP.
+- **Put proxy credentials in their own fields**, never inside the Proxy address box.
+- **SOCKS proxies cannot use a password.** Use HTTP or HTTPS if your proxy needs
+ a login.
+- **Changes apply to the next run.** You cannot edit a proxy while a run is
+ active — press **Stop** first.
+
+### 5.7 - Scheduled Tasks
+
+Lists runs you've queued for a future time. Each entry shows the time, which
+accounts, and a **Cancel** button.
+
+### 5.8 - How to schedule a run
+
+1. Tick the accounts you want.
+2. Set the date and time in **Schedule for Later**. It must be in the future,
+ and it's your computer's local clock.
+3. Click **Schedule Run**.
+
+**Two things have to be true for a scheduled run to actually fire:**
+
+- Scheduling must be switched on. It's **off by default** as a safety measure.
+ See [Section 6](#6-optional-turning-on-scheduling). If it's off, you'll get an
+ error message mentioning `API_ALLOW_SCHEDULE_WRITE`.
+- **Your server terminal must still be running when the time arrives**, and your
+ computer must be awake. Nothing can start a run if the waiter has gone home.
+
+### 5.9 - Live Logs
+
+The black console at the bottom. This is the bot narrating what it's doing, live.
+
+Each line has a timestamp, a level, and a message. The level is the useful part:
+
+| Level | Colour | What it means |
+| --------- | ------ | --------------------------------------------------- |
+| **INFO** | Normal | Routine progress. Ignore these. |
+| **WARN** | Yellow | Something was skipped or retried. Usually harmless. |
+| **ERROR** | Red | Something actually failed. Worth reading. |
+
+**Auto-scroll** (on by default) keeps the newest line in view. Turn it off when
+you want to scroll up and read something without being yanked back down.
+
+**Clear** empties the display. It only clears your view, not the server's
+records, so reloading the page brings recent history back.
+
+The console keeps the most recent 500 lines and replays the last 100 when you
+open the page, so you don't come back to a blank screen.
+
+**When something goes wrong, this is the first place to look.** Scroll to the
+red lines and read the message.
+
+---
+
+## 6. Optional: turning on scheduling
+
+Scheduling is disabled by default so that nothing can queue up runs on your
+machine without you deliberately allowing it. Turning it on is a one-line
+change.
+
+1. Stop the server: click its terminal window and press **Ctrl+C**.
+2. Open your `.env` file in Notepad.
+3. Add this line at the bottom:
+
+```env
+API_ALLOW_SCHEDULE_WRITE=true
+```
+
+4. Save and close.
+5. Start the server again with `npm run api`.
+
+**Schedule Run** now works.
+
+---
+
+## 7. Troubleshooting
+
+### The page says "Offline"
+
+The server isn't running or isn't reachable.
+
+- Check the terminal where you ran `npm run api`. Still open? Any red text?
+- If it's closed, run `npm run api` again.
+- Confirm the address is exactly `http://127.0.0.1:3010`.
+
+### "Port 3010 is already in use"
+
+A server is already running, probably from earlier. Either use the one that's
+already up (just open the page), or close the other terminal window and try
+again.
+
+### An account is stuck on "Not Logged In" or "Login Expired"
+
+Run the manual sign-in from Section 5.3. This fixes the large majority of
+sign-in problems.
+
+```bash
+npm run manual-login -- --email your.real.email@outlook.com
+```
+
+If it keeps expiring, delete the saved sessions and start clean:
+
+```bash
+npm run clear-sessions -- email your.real.email@outlook.com
+```
+
+Then sign in manually again.
+
+### "No accounts configured in .env yet"
+
+The email is in your browser's list but not in the `.env` file. Add it again
+through the **Add Account** box, which writes the file for you.
+
+### Run Selected does nothing
+
+- Did you tick at least one checkbox? Selecting accounts is separate from
+ running them.
+- Is the badge showing **Running**? Only one run happens at a time, so the
+ button is disabled while one is in progress.
+
+### Visual Search or Edge Browsing didn't happen
+
+Confirm the toggle was **on before** you clicked Run Selected, not after. Then
+check the Live Logs for a line like:
+
+```
+[Config] override: CONFIG_WORKER_VISUAL_SEARCH -> .workers.doVisualSearch = true
+```
+
+That line is the tool confirming it received your toggle. If it's there, the
+feature was switched on for that run. If a feature still gets skipped after
+that, the log will say why just below it, usually that the account had no such
+task available that day.
+
+### A command failed with lots of red text
+
+Try, in order:
+
+1. `npm run build` and then retry.
+2. If that fails, `npm run pre-build` followed by `npm run build`.
+3. Check that `config.json` and `.env` both exist and are named exactly right. A
+ missing `config.json` is one of the most common causes.
+
+### Browser windows were left open after a crash
+
+On Windows:
+
+```bash
+npm run kill-chrome-win
+```
+
+---
+
+## 8. Safety and privacy notes
+
+**Your credentials stay on your machine.** Passwords live only in your `.env`
+file on your own disk. The Web UI never asks for them, never displays them, and
+never sends them anywhere. Never share your `.env` file, and never post it in a
+screenshot or a support thread.
+
+**The dashboard has no password on it by default.** It's bound to `127.0.0.1`,
+which means only programs on your own computer can reach it. Anyone else on your
+Wi-Fi cannot. That's a deliberate default.
+
+If you ever change the server to listen on your network (with something like
+`--host 0.0.0.0`), **set a token first**, or anyone on that network can start
+runs on your accounts. Add a line like `API_TOKEN=some-long-random-string` to
+your `.env` before doing that.
+
+**Don't put `.env` or `config.json` into a public GitHub repository.** The
+project already tells Git to ignore both, so this only happens if you go out of
+your way. Just don't.
+
+**About the risk to your account.** Automating Microsoft Rewards is against
+Microsoft's terms of service. Accounts do get suspended or banned for it. This
+tool tries to behave like a person, with realistic delays, but there is no
+guarantee. Use it on an account you can afford to lose, and understand you're
+accepting that risk yourself.
+
+---
+
+## Quick reference
+
+**Daily startup:**
+
+```bash
+npm run api
+```
+
+Then open `http://127.0.0.1:3010`.
+
+**Common commands:**
+
+| Command | What it does |
+| ------------------------------------------------- | -------------------------------------- |
+| `npm run api` | Starts the server for the Web UI |
+| `npm run build` | Applies changes you've made to files |
+| `npm run manual-login -- --email you@example.com` | Signs an account in by hand |
+| `npm run clear-sessions -- list` | Shows saved sign-ins |
+| `npm run clear-sessions -- email you@example.com` | Deletes one account's saved sign-in |
+| `npm run kill-chrome-win` | Closes stuck browser windows (Windows) |
+
+**Key files:**
+
+| File | Holds |
+| ------------- | ------------------------------------------ |
+| `.env` | Your emails, passwords, and server options |
+| `config.json` | How the bot behaves |
+
+**Keyboard:** **Ctrl+C** in the server terminal stops the server.
diff --git a/WEB_UI_GUIDE.vi.md b/WEB_UI_GUIDE.vi.md
new file mode 100644
index 00000000..277eb1fb
--- /dev/null
+++ b/WEB_UI_GUIDE.vi.md
@@ -0,0 +1,530 @@
+# Microsoft Rewards Script - Hướng dẫn đầy đủ cho người mới bắt đầu
+
+Hướng dẫn từ đầu đến cuối cách cài đặt và sử dụng công cụ này thông qua **Web UI** mới. Không cần biết lập trình. Mọi lệnh đều chỉ cần sao chép, dán và nhấn Enter.
+
+> **Lưu ý:** các nút và tên mục trong giao diện vẫn giữ nguyên tiếng Anh (ví dụ **Run Selected**), vì Web UI chưa được dịch. Lệnh, tên file và tên biến cũng giữ nguyên tiếng Anh.
+
+---
+
+## Mục lục
+
+1. [Công cụ này thực sự làm gì](#1-công-cụ-này-thực-sự-làm-gì)
+2. [Yêu cầu trước khi bắt đầu](#2-yêu-cầu-trước-khi-bắt-đầu)
+3. [Cài đặt môi trường (chỉ làm một lần)](#3-cài-đặt-môi-trường-chỉ-làm-một-lần)
+4. [Khởi động Web UI](#4-khởi-động-web-ui)
+5. [Cách sử dụng Web UI](#5-cách-sử-dụng-web-ui)
+6. [Tùy chọn: bật tính năng lên lịch](#6-tùy-chọn-bật-tính-năng-lên-lịch)
+7. [Xử lý sự cố](#7-xử-lý-sự-cố)
+8. [Lưu ý về an toàn và bảo mật](#8-lưu-ý-về-an-toàn-và-bảo-mật)
+9. [Tra cứu nhanh](#tra-cứu-nhanh)
+
+---
+
+## 1. Công cụ này thực sự làm gì
+
+Hãy hình dung nó như **một con robot dùng trình duyệt web thay cho bạn**.
+
+Bình thường bạn kiếm điểm Microsoft Rewards bằng cách tìm kiếm trên Bing, bấm vào các thẻ nhiệm vụ hằng ngày và đọc bài viết. Công cụ này sẽ mở một trình duyệt thật, đăng nhập bằng tài khoản của bạn, rồi tự bấm và tự tìm kiếm.
+
+Có ba phần. Hình dung chúng như một nhà hàng sẽ dễ hiểu hơn:
+
+| Phần | Ví von nhà hàng | Thực chất là gì |
+| ---------- | --------------- | ----------------------------------------------------- |
+| **Bot** | Đầu bếp | Phần mở trình duyệt và thu thập điểm |
+| **Server** | Người phục vụ | Chương trình nhỏ nhận lệnh của bạn và báo lại kết quả |
+| **Web UI** | Thực đơn | Trang web bạn bấm vào trong trình duyệt |
+
+Bạn gần như chỉ cần đụng tới **thực đơn** (Web UI). Nhưng người phục vụ phải đang làm việc thì thực đơn mới có tác dụng, vì vậy ở bước 4 bạn sẽ khởi động server và để nó chạy suốt.
+
+---
+
+## 2. Yêu cầu trước khi bắt đầu
+
+### Máy tính của bạn cần có
+
+- **Windows, macOS hoặc Linux.** Hệ điều hành nào cũng được.
+- **Khoảng 2 GB dung lượng trống.** Công cụ sẽ tải về một bản trình duyệt riêng của nó, chiếm phần lớn dung lượng này.
+- **Kết nối internet.**
+- **Một màn hình.** Lần đăng nhập đầu tiên bạn cần nhìn thấy cửa sổ trình duyệt, nên bước đó không chạy được trên máy chủ không có màn hình.
+
+### Bạn cần chuẩn bị sẵn
+
+- **Một tài khoản Microsoft** đã bật Rewards. Hãy đăng nhập thử tại [rewards.bing.com](https://rewards.bing.com) một lần bằng trình duyệt thường ngày và chắc chắn nó hoạt động trước khi tự động hóa.
+- **Địa chỉ email của tài khoản.**
+- **Mật khẩu**, _chỉ khi_ tài khoản của bạn đăng nhập bằng mật khẩu. Nếu bạn dùng ứng dụng Microsoft Authenticator và không có mật khẩu thì bỏ qua.
+- **20 đến 30 phút** cho việc cài đặt một lần. Phần lớn thời gian là chờ tải xuống.
+
+### Phần mềm cần cài
+
+Chỉ một thứ duy nhất: **Node.js**, phiên bản 24 trở lên. Bước 3 sẽ hướng dẫn chi tiết.
+
+Node.js là bộ máy chạy công cụ này. Bạn không cần viết bất kỳ dòng code nào, giống như lái xe không đòi hỏi bạn phải tự chế tạo động cơ.
+
+> **Một lời trấn an:** bạn sẽ gõ lệnh vào một cửa sổ màu đen (gọi là "terminal"). Chuyện này hoàn toàn bình thường. Bạn không hề lập trình. Bạn chỉ đang đưa ra vài chỉ dẫn ngắn, giống như gõ địa chỉ trang web vào trình duyệt.
+
+---
+
+## 3. Cài đặt môi trường (chỉ làm một lần)
+
+Làm lần lượt năm bước sau. Mỗi bước dựa trên bước trước.
+
+### Bước 3.1 - Cài Node.js
+
+**Trên Windows:**
+
+1. Vào **[nodejs.org](https://nodejs.org)**.
+2. Tải bản **LTS** (nút màu xanh lớn). Kiểm tra số phiên bản bắt đầu bằng **24** hoặc cao hơn.
+3. Mở file vừa tải và bấm **Next** xuyên suốt trình cài đặt. Các lựa chọn mặc định là đúng, đừng thay đổi gì cả.
+4. Cài xong thì khởi động lại máy. Bước này quan trọng: Windows cần khởi động lại mới nhận ra Node.js.
+
+**Trên macOS:** tải file cài đặt `.pkg` bản LTS từ [nodejs.org](https://nodejs.org) và chạy nó.
+
+**Trên Linux:** dùng [nodesource](https://github.com/nodesource/distributions) hoặc trình quản lý gói của hệ thống, và xác nhận bạn có phiên bản 24 trở lên.
+
+**Kiểm tra đã cài thành công.** Mở terminal:
+
+- **Windows:** nhấn **phím Windows**, gõ `powershell`, nhấn Enter.
+- **macOS:** nhấn **Cmd+Space**, gõ `terminal`, nhấn Enter.
+- **Linux:** nhấn **Ctrl+Alt+T**.
+
+Gõ lệnh này và nhấn Enter:
+
+```bash
+node --version
+```
+
+Bạn sẽ thấy kết quả dạng `v24.5.0`. Nếu thấy "command not found" thì Node.js chưa cài đúng, hoặc bạn đã bỏ qua bước khởi động lại.
+
+> **Nếu số phiên bản nhỏ hơn 24**, nghĩa là máy đang có Node.js cũ. Hãy cài bản mới đè lên theo các bước ở trên.
+
+### Bước 3.2 - Tải công cụ về máy
+
+**Cách đơn giản nhất (không cần Git):**
+
+1. Vào trang GitHub của dự án.
+2. Bấm nút **Code** màu xanh, rồi chọn **Download ZIP**.
+3. Chuột phải vào file ZIP vừa tải, chọn **Extract All**.
+4. Di chuyển thư mục đã giải nén tới nơi bạn dễ nhớ, ví dụ thư mục Documents.
+
+> **Nên tránh các thư mục được đồng bộ bởi OneDrive, Dropbox hoặc Google Drive.** Chúng đôi khi khóa file trong lúc công cụ đang ghi vào, gây ra những lỗi rất khó hiểu.
+
+### Bước 3.3 - Đưa terminal vào đúng thư mục
+
+Terminal cần phải "đứng bên trong" thư mục dự án, giống như cách File Explorer mở một thư mục khi bạn bấm đúp vào nó.
+
+**Cách dễ nhất trên Windows:**
+
+1. Mở thư mục dự án trong File Explorer.
+2. Bấm vào thanh địa chỉ ở trên cùng (nơi hiện đường dẫn thư mục).
+3. Gõ `powershell` và nhấn Enter.
+
+Một terminal sẽ mở ra, đã nằm sẵn đúng vị trí.
+
+**Mọi hệ điều hành:** gõ `cd ` (có dấu cách), rồi kéo thư mục từ trình quản lý file thả vào cửa sổ terminal, sau đó nhấn Enter.
+
+**Kiểm tra bạn đang đứng đúng chỗ:**
+
+```bash
+ls
+```
+
+Bạn phải thấy các tên file, trong đó có `package.json` và `config.example.json`. Nếu không thấy thì bạn đang ở sai thư mục.
+
+> **Giữ nguyên cửa sổ terminal này.** Mọi lệnh từ đây về sau đều gõ vào đó.
+
+### Bước 3.4 - Cài các thành phần của công cụ
+
+Gõ lệnh này và nhấn Enter:
+
+```bash
+npm run pre-build
+```
+
+**Lệnh này chạy khá lâu.** Từ năm đến mười lăm phút là bình thường. Nó đang tải trình duyệt riêng của công cụ, dung lượng vài trăm megabyte.
+
+Bạn sẽ thấy rất nhiều chữ chạy trên màn hình. Điều đó không sao cả. Các cảnh báo màu vàng là bình thường và có thể bỏ qua. Chỉ cần lo khi nó kết thúc bằng chữ `error` và không có gì xảy ra tiếp.
+
+Sau đó chạy:
+
+```bash
+npm run build
+```
+
+Lệnh này nhanh, thường chưa tới một phút. Nó chuyển các file mã nguồn của dự án sang dạng mà máy tính thực sự chạy được.
+
+> **Hãy nhớ lệnh này.** Bất cứ khi nào bạn thay đổi file trong dự án, chạy lại `npm run build` để áp dụng thay đổi.
+
+### Bước 3.5 - Tạo hai file cấu hình
+
+Dự án đi kèm các file cấu hình **mẫu**. Bạn tạo bản sao của riêng mình rồi điền thông tin vào. Các file mẫu giống như một tờ đơn trống.
+
+**File 1: `config.json`** - điều khiển cách bot hoạt động.
+
+Tìm file tên `config.example.json` trong thư mục dự án. **Sao chép** nó, rồi đổi tên bản sao thành đúng `config.json`.
+
+- Windows: chuột phải → Copy, chuột phải → Paste, sau đó đổi tên `config.example - Copy.json` thành `config.json`.
+- Hãy chắc chắn tên file đúng là `config.json`, không còn chữ "example" hay "copy".
+
+Bạn không cần sửa gì bên trong file này. Các giá trị mặc định là đủ dùng.
+
+**File 2: `.env`** - chứa email và mật khẩu tài khoản của bạn.
+
+Tìm file tên `env.example`. Sao chép nó và đổi tên bản sao thành đúng `.env` - bắt đầu bằng một dấu chấm, và **không có** đuôi mở rộng.
+
+> **Mẹo cho Windows:** Explorer có thể không cho bạn đặt tên file bắt đầu bằng dấu chấm. Cách lách: đặt tên là `.env.` **có dấu chấm ở cuối** - Windows sẽ tự bỏ dấu chấm đó và để lại đúng `.env`.
+
+Bây giờ mở `.env` bằng **Notepad** (chuột phải → Open with → Notepad). Bạn sẽ thấy các dòng như thế này:
+
+```env
+ACCOUNT_1_EMAIL=email@example.com
+#ACCOUNT_1_PASSWORD=your_password
+```
+
+Đổi email thành email thật của bạn. Nếu tài khoản dùng mật khẩu, xóa dấu `#` ở đầu dòng mật khẩu và điền mật khẩu thật vào:
+
+```env
+ACCOUNT_1_EMAIL=your.real.email@outlook.com
+ACCOUNT_1_PASSWORD=YourRealPassword
+```
+
+**Dấu `#` nghĩa là "bỏ qua dòng này."** Xóa nó đi là bật dòng đó lên.
+
+Nếu tài khoản của bạn dùng ứng dụng Authenticator thay vì mật khẩu, cứ để nguyên dòng mật khẩu cùng dấu `#`. Công cụ sẽ tự xử lý.
+
+Lưu file và đóng Notepad.
+
+> **Nếu bạn dùng xác thực hai lớp**, có một dòng dành cho `ACCOUNT_1_TOTP_SECRET`. Điền vào đó sẽ giúp công cụ tự sinh mã 6 số cho bạn. Cách lấy giá trị: trong phần cài đặt bảo mật của Microsoft, mở "Manage how you sign in", thêm một ứng dụng authenticator, và khi mã QR hiện ra hãy chọn **"enter code manually"**. Dán đoạn mã đó làm giá trị.
+
+**Cài đặt đã xong.** Bạn không bao giờ phải lặp lại các bước từ 3.1 đến 3.5.
+
+---
+
+## 4. Khởi động Web UI
+
+Hai bước: đánh thức server, rồi mở trang web.
+
+### Bước 4.1 - Khởi động server
+
+Trong terminal của bạn (vẫn đang ở thư mục dự án), chạy:
+
+```bash
+npm run api
+```
+
+Bạn sẽ thấy vài dòng chữ khởi động, sau đó terminal có vẻ như **đứng yên**, không in ra gì thêm và không hiện dấu nhắc lệnh.
+
+**Điều đó là đúng.** Server giờ đang chạy và đang lắng nghe. Nó được thiết kế để nằm yên như vậy. Terminal không hề bị treo.
+
+> **Hãy để cửa sổ terminal này mở, đừng đóng nó.** Đóng lại là tắt server, và Web UI sẽ ngừng hoạt động. Thay vào đó hãy thu nhỏ cửa sổ.
+
+### Bước 4.2 - Mở trang web
+
+Mở trình duyệt bạn vẫn dùng hằng ngày (Chrome, Edge, Firefox, gì cũng được) và vào địa chỉ:
+
+```
+http://127.0.0.1:3010
+```
+
+`127.0.0.1` luôn có nghĩa là "chính máy tính này". Bạn không truy cập internet, chỉ là đang nối vào máy của mình.
+
+Bạn sẽ thấy bảng điều khiển **Microsoft Rewards Control**.
+
+Nhìn lên góc trên bên phải. Ở đó có nhãn **Server**:
+
+| Nhãn | Ý nghĩa |
+| ----------- | -------------------------------------------------------------------------- |
+| **Online** | Server đang chạy, chưa có phiên nào đang chạy. Đây là trạng thái bạn muốn. |
+| **Running** | Đang có một phiên chạy ngay lúc này. |
+| **Offline** | Trang web không kết nối được với server. Xem lại terminal ở bước 4.1. |
+
+Nếu hiện Offline, nhiều khả năng terminal của server đã bị đóng hoặc đã báo lỗi.
+
+### Mỗi lần dùng sau này
+
+Khởi động công cụ chỉ gồm đúng hai bước đó:
+
+1. Mở terminal trong thư mục dự án, chạy `npm run api`
+2. Mở `http://127.0.0.1:3010`
+
+---
+
+## 5. Cách sử dụng Web UI
+
+Trang web là một cột duy nhất gồm các mục, từ trên xuống dưới. Dưới đây là từng mục.
+
+### 5.1 - Add Account (Thêm tài khoản)
+
+Ô ở trên cùng. Gõ địa chỉ email rồi bấm **Add Account**.
+
+Việc này sẽ tự ghi email vào file `.env` cho bạn, nên bạn không phải sửa tay file đó nữa. Tài khoản xuất hiện trong danh sách ngay lập tức.
+
+> **Quan trọng: giao diện cố ý không bao giờ hỏi mật khẩu của bạn.** Mật khẩu chỉ tồn tại trong file `.env` trên chính ổ đĩa của bạn. Nếu tài khoản vừa thêm cần mật khẩu, hãy mở `.env` bằng Notepad và thêm dòng tương ứng. Với tài khoản được thêm ở vị trí `ACCOUNT_2`, dòng cần thêm là `ACCOUNT_2_PASSWORD=yourpassword`.
+
+### 5.2 - Account Overview (Tổng quan tài khoản)
+
+Bốn ô đếm cho thấy tình trạng các tài khoản của bạn.
+
+| Ô đếm | Ý nghĩa |
+| ------------------ | --------------------------------------------------------- |
+| **Total Accounts** | Công cụ biết bao nhiêu tài khoản |
+| **Logged In** | Đã có phiên đăng nhập hợp lệ. Sẵn sàng chạy. |
+| **Not Logged In** | Chưa từng đăng nhập. Cần làm bước bên dưới. |
+| **Login Expired** | Từng đăng nhập nhưng phiên đã hết hạn. Cần đăng nhập lại. |
+
+**Một tài khoản mới tinh sẽ bắt đầu ở trạng thái "Not Logged In". Đó là chuyện bình thường.**
+
+### 5.3 - Đăng nhập lần đầu cho tài khoản mới
+
+Microsoft sẽ không để một con robot vượt qua bước đăng nhập mới một cách trơn tru, nên lần đầu tiên bạn phải tự đăng nhập. Chỉ một lần. Sau đó phiên đăng nhập sẽ được lưu lại.
+
+Mở một terminal **thứ hai** trong thư mục dự án (để yên terminal của server) và chạy lệnh này, thay bằng email của bạn:
+
+```bash
+npm run manual-login -- --email your.real.email@outlook.com
+```
+
+Một cửa sổ trình duyệt sẽ mở ra. Đăng nhập bình thường, y như mọi ngày, kể cả khi được hỏi mã 2FA. Khi bạn vào tới trang Rewards, **hãy đợi khoảng năm giây**. Cửa sổ sẽ tự đóng và phiên đăng nhập của bạn được lưu.
+
+Quay lại bảng điều khiển, tài khoản đó sẽ chuyển sang **Logged In** trong vòng vài giây.
+
+> Hãy làm lại bước này mỗi khi tài khoản hiện **Login Expired**. Đây là cách sửa chuẩn cho gần như mọi vấn đề đăng nhập.
+
+### 5.4 - Execution Settings (Cài đặt thực thi)
+
+Bốn nút điều khiển quyết định phiên chạy tiếp theo sẽ hoạt động _như thế nào_. **Hãy chỉnh chúng trước khi bấm Run.** Lựa chọn của bạn được trình duyệt ghi nhớ cho lần sau.
+
+**Run in Headless Mode (Chạy ở chế độ ẩn)**
+
+- **Tắt** (mặc định): bạn nhìn thấy các cửa sổ trình duyệt mở ra và tự bấm.
+- **Bật**: mọi thứ diễn ra vô hình ở nền.
+
+Để tắt thì tốt hơn khi bạn mới làm quen, vì bạn nhìn thấy chuyện gì đang xảy ra. Bật lên thì tiện hơn khi bạn đã tin tưởng nó, để các cửa sổ không giật mất màn hình lúc bạn đang làm việc khác.
+
+> Đừng dùng chế độ ẩn cho lần đăng nhập đầu tiên. Hãy dùng `manual-login` ở mục 5.3.
+
+**Run Visual Search (Chạy tìm kiếm bằng hình ảnh)**
+
+Mặc định là tắt. Bật lên để làm thêm các nhiệm vụ Bing Visual Search (loại nhiệm vụ tìm kiếm bằng hình ảnh thay vì bằng chữ) nhằm kiếm thêm điểm hằng ngày.
+
+**Run 30-Minute Edge Browsing (Chạy phiên duyệt Edge 30 phút)**
+
+Mặc định là tắt. Bật lên để hoàn thành phần thưởng duyệt Edge, khi đó công cụ sẽ duyệt web ở nền trong nửa giờ.
+
+> **Tính năng này làm phiên chạy kéo dài thêm ít nhất 30 phút.** Đó là yêu cầu của chính phần thưởng, không phải công cụ chạy chậm. Nó được đánh dấu là thử nghiệm vì Microsoft thỉnh thoảng thay đổi cách hoạt động.
+
+**Schedule for Later (Lên lịch để chạy sau)**
+
+Một ô chọn ngày và giờ. Hãy để **trống** nếu muốn chạy ngay. Chỉ điền vào khi bạn định dùng nút Schedule Run (xem mục 5.7).
+
+### 5.5 - Ready to Execute (Sẵn sàng thực thi)
+
+Danh sách tài khoản của bạn, cùng các nút hành động.
+
+Mỗi dòng tài khoản hiển thị một **ô checkbox**, **email**, số lần đã **chạy**, thời điểm chạy **gần nhất**, số **điểm**, và một **nhãn trạng thái**.
+
+**Cách chạy:**
+
+1. **Tích vào ô checkbox** của từng tài khoản bạn muốn chạy. Hoặc bấm **Select All** để tích hết (nút này sẽ đổi thành **Deselect All**).
+2. Kiểm tra lại các công tắc trong Execution Settings.
+3. Bấm **Run Selected**.
+
+Một thông báo nhỏ sẽ trượt ra xác nhận đã bắt đầu, nhãn Server chuyển sang **Running**, và các dòng log bắt đầu hiện ở cuối trang.
+
+**Điều nên biết:** một phiên chạy đầy đủ mất khá lâu, thường từ 20 đến 60 phút cho mỗi tài khoản, lâu hơn nếu bật Edge Browsing. Công cụ cố tình tạm nghỉ giữa các thao tác để hành xử giống người thật hơn là giống máy móc. Chậm là có chủ đích.
+
+**Nút Stop** bị mờ đi trừ khi thực sự có gì đó đang chạy. Bấm vào đó để kết thúc phiên chạy: nó yêu cầu bot tự đóng trình duyệt một cách gọn gàng, và nếu bot không phản hồi kịp thời thì server sẽ buộc đóng. Dù theo cách nào cũng không để sót cửa sổ trình duyệt mồ côi.
+
+**Nút Remove** chỉ gỡ tài khoản khỏi danh sách này, nhưng **không xóa nó khỏi file `.env`**, nên tài khoản sẽ xuất hiện lại ở lần làm mới trang kế tiếp. Muốn xóa hẳn một tài khoản, hãy mở `.env` bằng Notepad và xóa dòng `ACCOUNT_N_EMAIL` của nó.
+
+### 5.6 - Proxy cho từng tài khoản
+
+Mỗi dòng tài khoản có một nút **Proxy**. Bấm vào đó để mở khung sửa proxy ngay bên dưới danh sách.
+
+Khung này cho bạn nhập **Proxy address**, **Port**, **Username**, **Password**, và một công tắc **Use for API requests too**. Sau khi lưu, dòng tài khoản sẽ hiện một **huy hiệu xanh** `host:port` để bạn biết tài khoản đó đã có proxy.
+
+**Bạn chỉ cần quan tâm mục này nếu chạy nhiều tài khoản.** Nếu chỉ có 1 tài khoản thì bỏ qua.
+
+> 📖 **Proxy là gì, chọn loại nào, mua ở đâu, sửa lỗi ra sao** — xem tài liệu riêng: **`PROXY_GUIDE.vi.md`**. Tài liệu đó viết riêng cho người chưa từng dùng proxy bao giờ.
+
+**Bốn điều cần nhớ ngay:**
+
+- **Một tài khoản phải dùng một proxy riêng.** Nhiều tài khoản dùng chung một proxy thì proxy mất hết tác dụng.
+- **Tài khoản/mật khẩu proxy để ở ô riêng**, không nhập vào ô Proxy address.
+- **SOCKS proxy không dùng được mật khẩu** — chỉ dùng HTTP hoặc HTTPS nếu proxy có đăng nhập.
+- **Thay đổi chỉ áp dụng cho lần chạy tới.** Tool đang chạy thì không sửa được, phải bấm **Stop** trước.
+
+### 5.7 - Scheduled Tasks (Các tác vụ đã lên lịch)
+
+Liệt kê các phiên chạy bạn đã xếp hàng cho một thời điểm trong tương lai. Mỗi mục hiển thị giờ chạy, những tài khoản nào, và một nút **Cancel**.
+
+### 5.8 - Cách lên lịch một phiên chạy
+
+1. Tích chọn các tài khoản bạn muốn.
+2. Đặt ngày và giờ trong **Schedule for Later**. Thời điểm đó phải ở tương lai, và tính theo đồng hồ của chính máy bạn.
+3. Bấm **Schedule Run**.
+
+**Có hai điều kiện phải đúng thì phiên chạy đã lên lịch mới thực sự khởi động:**
+
+- Tính năng lên lịch phải được bật. Nó **mặc định là tắt** như một biện pháp an toàn. Xem [mục 6](#6-tùy-chọn-bật-tính-năng-lên-lịch). Nếu đang tắt, bạn sẽ nhận thông báo lỗi có nhắc tới `API_ALLOW_SCHEDULE_WRITE`.
+- **Terminal của server vẫn phải đang chạy khi đến giờ**, và máy tính phải đang ở trạng thái thức. Người phục vụ đã về nhà thì không ai bắt đầu phiên chạy được.
+
+### 5.9 - Live Logs (Log trực tiếp)
+
+Bảng console màu đen ở cuối trang. Đây là nơi bot tường thuật những gì nó đang làm, theo thời gian thực.
+
+Mỗi dòng có thời điểm, mức độ và nội dung thông báo. Phần mức độ là phần hữu ích nhất:
+
+| Mức độ | Màu sắc | Ý nghĩa |
+| --------- | ----------- | ---------------------------------------------------- |
+| **INFO** | Bình thường | Tiến trình thông thường. Có thể bỏ qua. |
+| **WARN** | Vàng | Có gì đó bị bỏ qua hoặc được thử lại. Thường vô hại. |
+| **ERROR** | Đỏ | Thực sự có lỗi. Đáng để đọc. |
+
+**Auto-scroll** (mặc định bật) giữ dòng mới nhất luôn nằm trong tầm nhìn. Hãy tắt nó khi bạn muốn cuộn lên đọc lại thứ gì đó mà không bị kéo tuột xuống dưới.
+
+**Clear** xóa trắng phần hiển thị. Nó chỉ xóa những gì bạn đang thấy, không xóa dữ liệu trên server, nên tải lại trang sẽ mang phần lịch sử gần đây trở lại.
+
+Console giữ tối đa 500 dòng gần nhất và phát lại 100 dòng cuối khi bạn mở trang, để bạn không quay lại một màn hình trống trơn.
+
+**Khi có chuyện trục trặc, đây là nơi đầu tiên cần xem.** Cuộn tới các dòng màu đỏ và đọc nội dung.
+
+---
+
+## 6. Tùy chọn: bật tính năng lên lịch
+
+Tính năng lên lịch mặc định bị tắt, để không gì có thể tự xếp hàng phiên chạy trên máy của bạn nếu bạn không chủ động cho phép. Bật nó lên chỉ cần thêm một dòng.
+
+1. Dừng server: bấm vào cửa sổ terminal của nó và nhấn **Ctrl+C**.
+2. Mở file `.env` bằng Notepad.
+3. Thêm dòng này vào cuối file:
+
+```env
+API_ALLOW_SCHEDULE_WRITE=true
+```
+
+4. Lưu và đóng lại.
+5. Khởi động server lần nữa bằng `npm run api`.
+
+Giờ **Schedule Run** đã hoạt động.
+
+---
+
+## 7. Xử lý sự cố
+
+### Trang web báo "Offline"
+
+Server không chạy hoặc không kết nối được.
+
+- Kiểm tra terminal nơi bạn chạy `npm run api`. Còn mở không? Có chữ màu đỏ nào không?
+- Nếu nó đã bị đóng, chạy lại `npm run api`.
+- Xác nhận địa chỉ đúng là `http://127.0.0.1:3010`.
+
+### "Port 3010 is already in use"
+
+Đã có một server đang chạy, nhiều khả năng là từ trước đó. Hoặc dùng luôn cái đang chạy (chỉ cần mở trang web), hoặc đóng cửa sổ terminal kia rồi thử lại.
+
+### Một tài khoản cứ kẹt ở "Not Logged In" hoặc "Login Expired"
+
+Hãy chạy bước đăng nhập thủ công ở mục 5.3. Cách này sửa được phần lớn các vấn đề đăng nhập.
+
+```bash
+npm run manual-login -- --email your.real.email@outlook.com
+```
+
+Nếu nó cứ hết hạn mãi, hãy xóa các phiên đã lưu và bắt đầu sạch sẽ:
+
+```bash
+npm run clear-sessions -- email your.real.email@outlook.com
+```
+
+Rồi đăng nhập thủ công lại lần nữa.
+
+### "No accounts configured in .env yet"
+
+Email có trong danh sách trên trình duyệt của bạn nhưng chưa có trong file `.env`. Hãy thêm lại qua ô **Add Account**, ô đó sẽ ghi file giúp bạn.
+
+### Bấm Run Selected mà không thấy gì
+
+- Bạn đã tích ít nhất một ô checkbox chưa? Chọn tài khoản là việc riêng với chạy chúng.
+- Nhãn trạng thái có đang hiện **Running** không? Mỗi lần chỉ có một phiên chạy, nên nút này bị vô hiệu hóa khi đang có phiên chạy.
+
+### Visual Search hoặc Edge Browsing không diễn ra
+
+Hãy xác nhận công tắc đã **bật trước khi** bạn bấm Run Selected, chứ không phải sau đó. Rồi kiểm tra trong Live Logs xem có dòng như thế này không:
+
+```
+[Config] override: CONFIG_WORKER_VISUAL_SEARCH -> .workers.doVisualSearch = true
+```
+
+Dòng đó là công cụ xác nhận đã nhận được lựa chọn của bạn. Nếu nó có mặt, tính năng đã được bật cho phiên chạy đó. Nếu sau đó tính năng vẫn bị bỏ qua, phần log ngay bên dưới sẽ nói rõ lý do, thường là tài khoản không có nhiệm vụ đó trong ngày hôm ấy.
+
+### Một lệnh báo lỗi với rất nhiều chữ đỏ
+
+Hãy thử lần lượt:
+
+1. `npm run build` rồi thử lại.
+2. Nếu vẫn lỗi, chạy `npm run pre-build` rồi tới `npm run build`.
+3. Kiểm tra cả `config.json` và `.env` đều tồn tại và được đặt tên chính xác. Thiếu `config.json` là một trong những nguyên nhân phổ biến nhất.
+
+### Cửa sổ trình duyệt còn mở sau khi bị lỗi
+
+Trên Windows:
+
+```bash
+npm run kill-chrome-win
+```
+
+---
+
+## 8. Lưu ý về an toàn và bảo mật
+
+**Thông tin đăng nhập của bạn nằm lại trên máy bạn.** Mật khẩu chỉ tồn tại trong file `.env` trên chính ổ đĩa của bạn. Web UI không bao giờ hỏi, không bao giờ hiển thị và không bao giờ gửi chúng đi đâu cả. Đừng bao giờ chia sẻ file `.env`, và đừng đăng nó lên ảnh chụp màn hình hay một chủ đề hỗ trợ kỹ thuật.
+
+**Bảng điều khiển mặc định không có mật khẩu bảo vệ.** Nó được gắn vào `127.0.0.1`, nghĩa là chỉ các chương trình trên chính máy bạn mới truy cập được. Người khác trong cùng mạng Wi-Fi thì không. Đó là một thiết lập mặc định có chủ ý.
+
+Nếu bạn thay đổi để server lắng nghe trên cả mạng (bằng thứ gì đó như `--host 0.0.0.0`), **hãy đặt token trước**, nếu không bất kỳ ai trong mạng đó cũng có thể khởi động phiên chạy trên tài khoản của bạn. Thêm một dòng như `API_TOKEN=some-long-random-string` vào `.env` trước khi làm việc đó.
+
+**Đừng đưa `.env` hoặc `config.json` lên một kho GitHub công khai.** Dự án đã cấu hình để Git bỏ qua cả hai file này, nên chuyện đó chỉ xảy ra nếu bạn cố tình làm. Đừng làm.
+
+**Mật khẩu proxy cũng nằm trong `.env`.** Khi bạn lưu proxy qua Web UI, thông tin proxy được ghi vào `.env` dưới dạng các dòng `ACCOUNT_N_PROXY_*`. Web UI **không bao giờ hiển thị lại** mật khẩu proxy đã lưu — đó là lý do ô Password luôn trống mỗi khi bạn mở lại khung sửa. Cùng quy tắc như trên: đừng chia sẻ file này.
+
+**Về rủi ro với tài khoản của bạn.** Tự động hóa Microsoft Rewards là trái với điều khoản dịch vụ của Microsoft. Tài khoản thực sự có thể bị tạm khóa hoặc bị cấm vì việc này. Công cụ cố gắng hành xử giống người thật, với các khoảng nghỉ hợp lý, nhưng không có gì bảo đảm. Hãy dùng nó với một tài khoản mà bạn chấp nhận được nếu mất, và hiểu rằng bạn đang tự nhận lấy rủi ro đó.
+
+---
+
+## Tra cứu nhanh
+
+**Khởi động hằng ngày:**
+
+```bash
+npm run api
+```
+
+Rồi mở `http://127.0.0.1:3010`.
+
+**Các lệnh thường dùng:**
+
+| Lệnh | Tác dụng |
+| ------------------------------------------------- | -------------------------------------------- |
+| `npm run api` | Khởi động server cho Web UI |
+| `npm run build` | Áp dụng các thay đổi bạn đã làm vào file |
+| `npm run manual-login -- --email you@example.com` | Đăng nhập tài khoản bằng tay |
+| `npm run clear-sessions -- list` | Xem các phiên đăng nhập đã lưu |
+| `npm run clear-sessions -- email you@example.com` | Xóa phiên đăng nhập đã lưu của một tài khoản |
+| `npm run kill-chrome-win` | Đóng các cửa sổ trình duyệt bị kẹt (Windows) |
+
+**Các file quan trọng:**
+
+| File | Chứa gì |
+| ------------- | ---------------------------------------------- |
+| `.env` | Email, mật khẩu của bạn và các tùy chọn server |
+| `config.json` | Cách bot hoạt động |
+
+**Phím tắt:** nhấn **Ctrl+C** trong terminal của server để dừng server.
+
+**Tài liệu khác:**
+
+| File | Nội dung |
+| -------------------- | ------------------------------------------------------------------------ |
+| `WEB_UI_GUIDE.vi.md` | Hướng dẫn Web UI dành cho người mới (file này) |
+| `PROXY_GUIDE.vi.md` | Hướng dẫn cài đặt proxy từ A đến Z — loại nào, mua ở đâu, sửa lỗi ra sao |
diff --git a/eslint.config.mjs b/eslint.config.mjs
index c1e6c787..ea438f1a 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -25,6 +25,15 @@ export default tseslint.config(
'preserve-caught-error': 'off'
}
},
+ // Browser-side dashboard code
+ {
+ files: ['public/**/*.js'],
+ languageOptions: {
+ globals: {
+ ...globals.browser
+ }
+ }
+ },
// Must come last: disables ESLint rules that conflict with Prettier formatting
prettier
)
diff --git a/package.json b/package.json
index 9e736bc9..43f721cd 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,9 @@
"create-docker": "docker build -t microsoft-rewards-script-docker .",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
+ "test": "node --test scripts/api/logParser.test.js tests/*.test.mjs",
"test:log-parser": "node --test scripts/api/logParser.test.js",
+ "test:abort": "node --test tests/abort.test.mjs",
"format": "prettier --write .",
"format:check": "prettier --check .",
"clear-diagnostics": "rimraf diagnostics",
diff --git a/public/README.md b/public/README.md
new file mode 100644
index 00000000..78d62f72
--- /dev/null
+++ b/public/README.md
@@ -0,0 +1,66 @@
+# Web UI for Microsoft Rewards Script
+
+A simplified, accessible web interface for managing Microsoft Rewards accounts.
+
+## Features
+
+- **Add accounts with email only** - no coding knowledge required
+- **Real-time dashboard** - view server status and account statistics
+- **Account queue** - see all accounts ready to execute
+- **Live point balance** - track points for each account
+- **Status tracking** - see which accounts are logged in, expired, or not logged in
+- **Selective execution** - choose which accounts to run with checkboxes
+- **Responsive design** - works on desktop, tablet, and mobile
+
+## Setup
+
+1. Start the API server:
+
+```bash
+npm run api
+```
+
+2. Open your browser to:
+
+```
+http://127.0.0.1:3010
+```
+
+3. Add accounts using the email form
+4. Select accounts and click "Run Selected"
+
+## Configuration
+
+The Web UI connects to the Control API at `http://127.0.0.1:3010` by default.
+
+To change the API URL, edit `public/app.js`:
+
+```javascript
+const API_BASE_URL = 'http://your-server:3010'
+```
+
+## How It Works
+
+- Accounts are stored in browser localStorage for persistence
+- The UI polls the API every 5 seconds for status updates
+- Point balances and run history come from the Control API
+- Account credentials remain in your `.env` file (not exposed to the UI)
+
+## Adding Accounts to .env
+
+After adding an account via the UI, you still need to add credentials to `.env`:
+
+```env
+ACCOUNT_1_EMAIL=email@example.com
+ACCOUNT_1_PASSWORD=your_password
+```
+
+Then rebuild: `npm run build`
+
+## Accessibility
+
+- Keyboard navigable
+- Screen reader compatible
+- ARIA labels on interactive elements
+- High contrast color scheme
+- Focus indicators on all controls
diff --git a/public/app.js b/public/app.js
new file mode 100644
index 00000000..8f1eeddc
--- /dev/null
+++ b/public/app.js
@@ -0,0 +1,891 @@
+// Configuration
+const API_BASE_URL = 'http://127.0.0.1:3010'
+const POLL_INTERVAL = 5000 // 5 seconds
+const MAX_LOG_LINES = 500
+
+// State
+let accounts = []
+let selectedAccountIndexes = new Set()
+let scheduledTasks = []
+let logSource = null
+let proxyAccountIndex = null
+
+// DOM Elements
+const emailInput = document.getElementById('emailInput')
+const addAccountForm = document.getElementById('addAccountForm')
+const accountsList = document.getElementById('accountsList')
+const serverStatusEl = document.getElementById('serverStatus')
+const totalAccountsEl = document.getElementById('totalAccounts')
+const loggedInCountEl = document.getElementById('loggedInCount')
+const notLoggedInCountEl = document.getElementById('notLoggedInCount')
+const expiredCountEl = document.getElementById('expiredCount')
+const selectAllBtn = document.getElementById('selectAllBtn')
+const runSelectedBtn = document.getElementById('runSelectedBtn')
+const scheduleSelectedBtn = document.getElementById('scheduleSelectedBtn')
+const headlessToggle = document.getElementById('headlessToggle')
+const visualSearchToggle = document.getElementById('visualSearchToggle')
+const edgeBrowsingToggle = document.getElementById('edgeBrowsingToggle')
+const stopBtn = document.getElementById('stopBtn')
+const scheduleTimeInput = document.getElementById('scheduleTime')
+const scheduledList = document.getElementById('scheduledList')
+const logsConsole = document.getElementById('logsConsole')
+const autoScrollToggle = document.getElementById('autoScrollToggle')
+const clearLogsBtn = document.getElementById('clearLogsBtn')
+const toast = document.getElementById('toast')
+const proxyForm = document.getElementById('proxyForm')
+const proxyEmpty = document.getElementById('proxyEmpty')
+const proxyAccountEmail = document.getElementById('proxyAccountEmail')
+const proxyUrlInput = document.getElementById('proxyUrlInput')
+const proxyPortInput = document.getElementById('proxyPortInput')
+const proxyUsernameInput = document.getElementById('proxyUsernameInput')
+const proxyPasswordInput = document.getElementById('proxyPasswordInput')
+const proxyPasswordHint = document.getElementById('proxyPasswordHint')
+const proxyHttpToggle = document.getElementById('proxyHttpToggle')
+const proxyCancelBtn = document.getElementById('proxyCancelBtn')
+const proxyClearBtn = document.getElementById('proxyClearBtn')
+
+// Initialize
+document.addEventListener('DOMContentLoaded', () => {
+ loadFromLocalStorage()
+ loadScheduledTasks()
+ setupEventListeners()
+ checkServerHealth()
+ startPolling()
+ connectLogStream()
+
+ // Load saved run preferences
+ restoreToggle('headless_mode', headlessToggle)
+ restoreToggle('visual_search', visualSearchToggle)
+ restoreToggle('edge_browsing', edgeBrowsingToggle)
+})
+
+function restoreToggle(key, element) {
+ const saved = localStorage.getItem(key)
+ if (saved !== null) element.checked = saved === 'true'
+}
+
+function setupEventListeners() {
+ addAccountForm.addEventListener('submit', handleAddAccount)
+ selectAllBtn.addEventListener('click', handleSelectAll)
+ runSelectedBtn.addEventListener('click', handleRunSelected)
+ scheduleSelectedBtn.addEventListener('click', handleScheduleSelected)
+ headlessToggle.addEventListener('change', handleHeadlessToggle)
+ visualSearchToggle.addEventListener('change', () =>
+ localStorage.setItem('visual_search', visualSearchToggle.checked)
+ )
+ edgeBrowsingToggle.addEventListener('change', () =>
+ localStorage.setItem('edge_browsing', edgeBrowsingToggle.checked)
+ )
+ stopBtn.addEventListener('click', handleStop)
+ clearLogsBtn.addEventListener('click', handleClearLogs)
+ proxyForm.addEventListener('submit', handleSaveProxy)
+ proxyCancelBtn.addEventListener('click', closeProxyEditor)
+ proxyClearBtn.addEventListener('click', handleClearProxy)
+}
+
+// Proxy editor — edits ACCOUNT_N_PROXY_* in .env through the control API.
+function openProxyEditor(index) {
+ const account = accounts.find(acc => acc.index === index)
+ if (!account) return
+
+ proxyAccountIndex = index
+ proxyAccountEmail.textContent = account.email
+ proxyEmpty.hidden = true
+ proxyForm.hidden = false
+
+ // Reset first so a failed load cannot leave the previous account's values on screen.
+ proxyUrlInput.value = ''
+ proxyPortInput.value = ''
+ proxyUsernameInput.value = ''
+ proxyPasswordInput.value = ''
+ proxyHttpToggle.checked = false
+
+ fetch(`${API_BASE_URL}/accounts/${index}/proxy`)
+ .then(response => (response.ok ? response.json() : Promise.reject(new Error('load failed'))))
+ .then(data => {
+ // Ignore a response that arrived after the user switched accounts.
+ if (proxyAccountIndex !== index) return
+ const proxy = data.proxy || {}
+ proxyUrlInput.value = proxy.url || ''
+ proxyPortInput.value = proxy.port ? String(proxy.port) : ''
+ proxyUsernameInput.value = proxy.username || ''
+ proxyHttpToggle.checked = Boolean(proxy.proxyHttp)
+ proxyPasswordHint.textContent = proxy.hasPassword
+ ? 'A password is saved. Leave blank to keep it.'
+ : 'No password saved yet.'
+ })
+ .catch(() => showToast('Could not load the saved proxy', 'error'))
+
+ proxyForm.scrollIntoView({ behavior: 'smooth', block: 'center' })
+}
+
+function closeProxyEditor() {
+ proxyAccountIndex = null
+ proxyForm.hidden = true
+ proxyEmpty.hidden = false
+ proxyAccountEmail.textContent = 'an account'
+}
+
+function proxyPayloadFromForm() {
+ const body = {
+ url: proxyUrlInput.value.trim(),
+ username: proxyUsernameInput.value.trim(),
+ proxyHttp: proxyHttpToggle.checked
+ }
+ // Omit blanks: the port field is only meaningful with a URL, and an empty
+ // password means "keep the stored one".
+ const port = proxyPortInput.value.trim()
+ if (port) body.port = Number(port)
+ if (proxyPasswordInput.value) body.password = proxyPasswordInput.value
+ return body
+}
+
+async function saveProxy(body, successMessage) {
+ const index = proxyAccountIndex
+ if (index == null) return false
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/accounts/${index}/proxy`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body)
+ })
+ const data = await response.json().catch(() => ({}))
+
+ if (!response.ok) {
+ showToast(data.error || 'Failed to save the proxy', 'error')
+ return false
+ }
+
+ showToast(successMessage, 'success')
+ closeProxyEditor()
+ await checkServerHealth()
+ return true
+ } catch {
+ showToast('Failed to connect to server', 'error')
+ return false
+ }
+}
+
+async function handleSaveProxy(e) {
+ e.preventDefault()
+ await saveProxy(proxyPayloadFromForm(), 'Proxy saved - it applies to the next run')
+}
+
+async function handleClearProxy() {
+ if (!confirm('Remove the proxy for this account?')) return
+ await saveProxy({ url: '', proxyHttp: false }, 'Proxy removed')
+}
+
+// Live log stream (Server-Sent Events)
+function connectLogStream() {
+ if (logSource) logSource.close()
+
+ logsConsole.innerHTML = '
Connecting to log stream...
'
+
+ const source = new EventSource(`${API_BASE_URL}/events?replay=100`)
+ logSource = source
+
+ source.addEventListener('hello', event => {
+ const status = JSON.parse(event.data)
+ updateServerStatus(status.state === 'running' ? 'running' : 'online')
+ logsConsole.innerHTML = ''
+ })
+
+ source.addEventListener('log', event => {
+ appendLogLine(JSON.parse(event.data))
+ })
+
+ source.addEventListener('status', event => {
+ const status = JSON.parse(event.data)
+ updateServerStatus(status.state === 'running' ? 'running' : 'online')
+ })
+
+ source.onerror = () => {
+ updateServerStatus('offline')
+ // EventSource reconnects on its own; surface the gap without spamming.
+ if (!logsConsole.querySelector('.log-empty')) {
+ appendLogLine({ level: 'warn', message: 'Log stream disconnected - reconnecting...' })
+ }
+ }
+
+ window.addEventListener('beforeunload', () => source.close())
+}
+
+function appendLogLine(entry) {
+ const empty = logsConsole.querySelector('.log-empty')
+ if (empty) empty.remove()
+
+ const line = document.createElement('div')
+ line.className = 'log-line'
+ line.dataset.level = entry.level || 'info'
+
+ const time = document.createElement('span')
+ time.className = 'log-time'
+ time.textContent = formatLogTime(entry)
+
+ const level = document.createElement('span')
+ level.className = 'log-level'
+ level.textContent = (entry.level || 'info').toUpperCase()
+
+ const message = document.createElement('span')
+ message.className = 'log-message'
+ message.textContent = entry.title ? `[${entry.title}] ${entry.message ?? ''}` : (entry.message ?? '')
+
+ line.append(time, level, message)
+ logsConsole.append(line)
+
+ while (logsConsole.childElementCount > MAX_LOG_LINES) {
+ logsConsole.firstElementChild.remove()
+ }
+
+ const shouldScroll = autoScrollToggle.checked
+ const nearBottom = logsConsole.scrollHeight - logsConsole.scrollTop - logsConsole.clientHeight < 80
+ if (shouldScroll && nearBottom) {
+ logsConsole.scrollTop = logsConsole.scrollHeight
+ }
+}
+
+function formatLogTime(entry) {
+ const raw = entry.ts || entry.receivedAt
+ if (!raw) return '--:--:--'
+ const date = new Date(raw)
+ if (Number.isNaN(date.getTime())) return '--:--:--'
+ return date.toLocaleTimeString(undefined, { hour12: false })
+}
+
+function handleClearLogs() {
+ logsConsole.innerHTML = ''
+}
+
+// API Functions
+async function checkServerHealth() {
+ try {
+ const response = await fetch(`${API_BASE_URL}/health`)
+ const data = await response.json()
+
+ if (data.ok) {
+ updateServerStatus(data.state === 'running' ? 'running' : 'online')
+ await fetchAccounts()
+ }
+ } catch {
+ updateServerStatus('offline')
+ }
+}
+
+async function fetchAccounts() {
+ try {
+ const response = await fetch(`${API_BASE_URL}/accounts`)
+ const data = await response.json()
+
+ if (data.accounts) {
+ const apiAccounts = new Map()
+ const apiAccountEmails = new Set()
+
+ // Map API accounts by email - session-derived status wins so the
+ // badge reflects live cookies, not empty post-restart run history.
+ for (const acc of data.accounts) {
+ apiAccountEmails.add(acc.email)
+ apiAccounts.set(acc.email, {
+ index: acc.index,
+ email: acc.email,
+ points: acc.lastCollected || 0,
+ status: acc.sessionStatus ? acc.sessionStatus : determineAccountStatus(acc),
+ sessionStatus: acc.sessionStatus ?? null,
+ sessionUpdatedAt: acc.sessionUpdatedAt ?? null,
+ runs: acc.runs || 0,
+ lastRunAt: acc.lastRunAt,
+ lastSuccess: acc.lastSuccess,
+ isConfigured: true // Mark as configured in API
+ })
+ }
+
+ // Merge: update existing localStorage accounts with API data
+ accounts = accounts.map(localAcc => {
+ const apiData = apiAccounts.get(localAcc.email)
+ if (apiData) {
+ // Account exists in API, merge live data
+ return { ...localAcc, ...apiData }
+ }
+ // Account only in localStorage (not yet in .env)
+ return { ...localAcc, isConfigured: false }
+ })
+
+ // Adopt accounts that exist in .env but were never seen by this browser
+ const knownEmails = new Set(accounts.map(acc => acc.email))
+ for (const [email, apiData] of apiAccounts) {
+ if (!knownEmails.has(email)) accounts.push(apiData)
+ }
+
+ saveToLocalStorage()
+ renderAccounts()
+ updateStats()
+ }
+ } catch (error) {
+ console.error('Failed to fetch accounts:', error)
+ }
+}
+
+function determineAccountStatus(account) {
+ if (!account.lastRunAt) return 'not-logged-in'
+ if (account.lastSuccess === true) return 'logged-in'
+ if (account.lastSuccess === false) return 'expired'
+ return 'not-logged-in'
+}
+
+async function startMultipleAccounts(accountIndexes, options = {}) {
+ try {
+ const { headless = false, visualSearch = false, edgeBrowsing = false } = options
+
+ // Only work with accounts that are configured in the API
+ const apiAccounts = accounts.filter(acc => acc.isConfigured)
+
+ if (apiAccounts.length === 0) {
+ showToast('No accounts configured in .env yet. Add credentials and rebuild.', 'warning')
+ return
+ }
+
+ // Filter selected indexes to only include API-backed accounts
+ const validIndexes = accountIndexes.filter(idx => apiAccounts.some(acc => acc.index === idx))
+
+ if (validIndexes.length === 0) {
+ showToast('Selected accounts are not configured in .env yet', 'warning')
+ return
+ }
+
+ const allApiIndexes = apiAccounts.map(acc => acc.index)
+ const excludedIndexes = allApiIndexes.filter(idx => !validIndexes.includes(idx))
+
+ // These map onto config.json paths; both features are off in config by
+ // default, so without the overrides they never run.
+ const body = {
+ env: {
+ CONFIG_HEADLESS: headless ? 'true' : 'false',
+ CONFIG_WORKER_VISUAL_SEARCH: visualSearch ? 'true' : 'false',
+ CONFIG_EXPERIMENTAL_EDGE_BROWSING: edgeBrowsing ? 'true' : 'false'
+ }
+ }
+
+ if (excludedIndexes.length > 0) {
+ body.excludedAccountIndexes = excludedIndexes
+ }
+
+ const response = await fetch(`${API_BASE_URL}/start`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body)
+ })
+
+ const data = await response.json()
+
+ if (response.ok) {
+ showToast(`Started ${validIndexes.length} account(s)${headless ? ' in headless mode' : ''}`, 'success')
+ await checkServerHealth()
+ return true
+ } else {
+ showToast(data.error || 'Failed to start accounts', 'error')
+ return false
+ }
+ } catch {
+ showToast('Failed to connect to server', 'error')
+ return false
+ }
+}
+
+// Event Handlers
+async function handleAddAccount(e) {
+ e.preventDefault()
+
+ const email = emailInput.value.trim()
+
+ if (!email) {
+ showToast('Please enter an email address', 'error')
+ return
+ }
+
+ if (accounts.some(acc => acc.email.toLowerCase() === email.toLowerCase())) {
+ showToast('Account already exists', 'warning')
+ return
+ }
+
+ const submitBtn = addAccountForm.querySelector('button[type="submit"]')
+ submitBtn.disabled = true
+ submitBtn.textContent = 'Adding...'
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/accounts`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email })
+ })
+ const data = await response.json()
+
+ if (response.ok) {
+ accounts.push({
+ index: data.index,
+ email: data.email,
+ points: 0,
+ status: 'not-logged-in',
+ runs: 0,
+ lastRunAt: null,
+ lastSuccess: null,
+ isConfigured: true
+ })
+ saveToLocalStorage()
+ renderAccounts()
+ updateStats()
+ emailInput.value = ''
+ showToast(`${data.email} added and ready to run`, 'success')
+ } else {
+ showToast(data.error || 'Failed to add account', 'error')
+ }
+ } catch {
+ showToast('Failed to connect to server', 'error')
+ }
+
+ submitBtn.disabled = false
+ submitBtn.textContent = 'Add Account'
+}
+
+function handleSelectAll() {
+ if (selectedAccountIndexes.size === accounts.length) {
+ selectedAccountIndexes.clear()
+ selectAllBtn.textContent = 'Select All'
+ } else {
+ accounts.forEach(acc => selectedAccountIndexes.add(acc.index))
+ selectAllBtn.textContent = 'Deselect All'
+ }
+ renderAccounts()
+}
+
+async function handleRunSelected() {
+ if (selectedAccountIndexes.size === 0) {
+ showToast('Please select at least one account', 'warning')
+ return
+ }
+
+ runSelectedBtn.disabled = true
+ runSelectedBtn.textContent = 'Starting...'
+
+ const indexArray = Array.from(selectedAccountIndexes)
+ const success = await startMultipleAccounts(indexArray, {
+ headless: headlessToggle.checked,
+ visualSearch: visualSearchToggle.checked,
+ edgeBrowsing: edgeBrowsingToggle.checked
+ })
+
+ runSelectedBtn.disabled = false
+ runSelectedBtn.textContent = 'Run Selected'
+
+ if (success) {
+ selectedAccountIndexes.clear()
+ selectAllBtn.textContent = 'Select All'
+ renderAccounts()
+ }
+}
+
+async function handleScheduleSelected() {
+ if (selectedAccountIndexes.size === 0) {
+ showToast('Please select at least one account', 'warning')
+ return
+ }
+
+ const scheduleTime = scheduleTimeInput.value
+ if (!scheduleTime) {
+ showToast('Please select a date and time', 'warning')
+ return
+ }
+
+ const scheduledDate = new Date(scheduleTime)
+ const now = new Date()
+
+ if (scheduledDate <= now) {
+ showToast('Schedule time must be in the future', 'warning')
+ return
+ }
+
+ scheduleSelectedBtn.disabled = true
+ scheduleSelectedBtn.textContent = 'Scheduling...'
+
+ const indexArray = Array.from(selectedAccountIndexes)
+ const headless = headlessToggle.checked
+ const visualSearch = visualSearchToggle.checked
+ const edgeBrowsing = edgeBrowsingToggle.checked
+ const accountEmails = accounts.filter(acc => indexArray.includes(acc.index)).map(acc => acc.email)
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/schedule/tasks`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ accountIndexes: indexArray,
+ scheduledAt: scheduledDate.toISOString(),
+ headless,
+ visualSearch,
+ edgeBrowsing
+ })
+ })
+
+ const data = await response.json()
+
+ if (response.ok) {
+ const task = {
+ id: data.task.id,
+ accountIndexes: indexArray,
+ accountEmails,
+ scheduledAt: scheduledDate.toISOString(),
+ headless,
+ visualSearch,
+ edgeBrowsing,
+ createdAt: new Date().toISOString()
+ }
+
+ scheduledTasks.push(task)
+ saveScheduledTasks()
+ renderScheduledTasks()
+
+ const timeStr = scheduledDate.toLocaleString()
+ showToast(`Accounts scheduled to run at ${timeStr}`, 'success')
+
+ selectedAccountIndexes.clear()
+ selectAllBtn.textContent = 'Select All'
+ scheduleTimeInput.value = ''
+ renderAccounts()
+ } else {
+ showToast(data.error || 'Failed to schedule task', 'error')
+ }
+ } catch {
+ showToast('Failed to connect to server', 'error')
+ }
+
+ scheduleSelectedBtn.disabled = false
+ scheduleSelectedBtn.textContent = 'Schedule Run'
+}
+
+function handleHeadlessToggle() {
+ localStorage.setItem('headless_mode', headlessToggle.checked)
+}
+
+async function handleStop() {
+ stopBtn.disabled = true
+ stopBtn.textContent = 'Stopping...'
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/stop`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ force: false })
+ })
+
+ if (response.ok) {
+ showToast('Stopping run - closing browsers...', 'warning')
+ } else {
+ const data = await response.json().catch(() => ({}))
+ showToast(data.error || 'Failed to stop the run', 'error')
+ }
+ } catch {
+ showToast('Failed to connect to server', 'error')
+ }
+
+ stopBtn.textContent = 'Stop'
+ // updateServerStatus re-enables it while a run is still active.
+ await checkServerHealth()
+}
+
+function handleAccountCheckbox(index, checked) {
+ if (checked) {
+ selectedAccountIndexes.add(index)
+ } else {
+ selectedAccountIndexes.delete(index)
+ }
+
+ selectAllBtn.textContent = selectedAccountIndexes.size === accounts.length ? 'Deselect All' : 'Select All'
+ renderAccounts()
+}
+
+async function handleDeleteAccount(index) {
+ const account = accounts.find(acc => acc.index === index)
+ if (!account) return
+
+ const label = account.email
+ if (!account.isConfigured) {
+ showToast(`${label} is not in .env yet, so there is nothing to remove.`, 'warning')
+ return
+ }
+ if (!confirm(`Remove ${label} from .env?\n\nThis also deletes its saved sign-in sessions.`)) return
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/accounts/${index}`, { method: 'DELETE' })
+ const data = await response.json().catch(() => ({}))
+
+ if (!response.ok) {
+ showToast(data.error || 'Failed to remove the account', 'error')
+ return
+ }
+
+ // Drop it locally now so the row goes before the next poll lands.
+ accounts = accounts.filter(acc => acc.index !== index)
+ selectedAccountIndexes.delete(index)
+ saveToLocalStorage()
+ renderAccounts()
+ updateStats()
+ const sessions = data.sessionsRemoved ? ` (${data.sessionsRemoved} session rows deleted)` : ''
+ showToast(`${label} removed${sessions}`, 'success')
+ await fetchAccounts()
+ } catch {
+ showToast('Failed to connect to server', 'error')
+ }
+}
+
+async function handleCancelScheduledTask(taskId) {
+ if (!confirm('Cancel this scheduled task?')) return
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/schedule/tasks/${taskId}`, {
+ method: 'DELETE'
+ })
+
+ if (response.ok) {
+ scheduledTasks = scheduledTasks.filter(task => task.id !== taskId)
+ saveScheduledTasks()
+ renderScheduledTasks()
+ showToast('Scheduled task cancelled', 'success')
+ } else {
+ showToast('Failed to cancel task', 'error')
+ }
+ } catch {
+ // If API doesn't support cancellation, remove locally
+ scheduledTasks = scheduledTasks.filter(task => task.id !== taskId)
+ saveScheduledTasks()
+ renderScheduledTasks()
+ showToast('Scheduled task cancelled', 'success')
+ }
+}
+
+// Rendering Functions
+function renderAccounts() {
+ if (accounts.length === 0) {
+ accountsList.innerHTML = `
+
+
No accounts configured. Add an account above to get started.