# Giới thiệu

Tôi là Nguyễn Hữu Quý, một đam mê trong lĩnh vực phát triển phần mềm với kinh nghiệm đặc biệt trong việc sử dụng ngôn ngữ lập trình C#.

## Nguyễn Hữu Quý

**Chào mừng đến với trang cá nhân của tôi!** Tôi là Nguyễn Hữu Quý, một đam mê trong lĩnh vực phát triển phần mềm với kinh nghiệm đặc biệt trong việc sử dụng ngôn ngữ lập trình C#. Tôi tự hào về khả năng giải quyết vấn đề, tư duy logic và khả năng làm việc nhóm hiệu quả. Dưới đây là một cái nhìn sâu hơn về bản thân tôi và sự nghiệp của mình.

### Thông Tin Cơ Bản

* **Vị Trí Hiện Tại:** Nhân viên phát triển phần mềm
* **Kỹ Năng Chính:** Lập trình C#, .NET Framework, .NET Core
* **Ngôn Ngữ Lập Trình Khác:** JavaScript, SQL, HTML/CSS
* **Học Vấn:** \[Thêm thông tin về trình độ học vấn của bạn]

### Kinh Nghiệm Làm Việc

Tôi đã có cơ hội làm việc trong một loạt các dự án, từ phát triển ứng dụng doanh nghiệp cho đến sản phẩm phần mềm người dùng cuối. Đây là một số dự án tiêu biểu mà tôi đã đóng góp:

* **Hệ Thống Quản Lý Doanh Nghiệp XYZ:** Phát triển các module quản lý nhân sự và tài chính, giúp cải thiện 20% hiệu suất công việc của doanh nghiệp.
* **Ứng Dụng Đặt Phòng Trực Tuyến ABC:** Làm việc cùng đội ngũ để thiết kế và triển khai ứng dụng, đạt được 10,000 đăng ký người dùng trong tháng đầu tiên.
* **Công Cụ Phân Tích Dữ Liệu QWE:** Phát triển công cụ phân tích dữ liệu sử dụng C# và .NET Core, giúp tăng cường khả năng đưa ra quyết định dựa trên dữ liệu cho khách hàng.

### Học Vấn

\[Thêm chi tiết về bằng cấp, khóa học và chứng chỉ liên quan. Ví dụ:

* Bằng Cử Nhân Công Nghệ Thông Tin, Đại học Công Nghệ XYZ, 201x-201y.
* Chứng chỉ Chuyên Môn C# và .NET, Học Viện Công Nghệ ABC, 201z.]

### Sở Thích và Hoạt Động

Ngoài giờ làm việc, tôi yêu thích tham gia các dự án mã nguồn mở, đọc sách về công nghệ và phát triển bản thân trong các lĩnh vực mới như Trí tuệ Nhân tạo và Học Máy. Tôi cũng đam mê thể thao và thường xuyên tham gia các hoạt động ngoại khóa liên quan đến bóng đá và bơi lội.

### Liên Hệ

* Email: \[Địa chỉ email của bạn]
* LinkedIn: \[Link LinkedIn của bạn]
* GitHub: \[Link GitHub của bạn]
* Blog Cá Nhân: \[Nếu có]

**Nếu bạn quan tâm đến việc tìm hiểu thêm về tôi hoặc cần sự hợp tác trong một dự án, đừng ngần ngại liên hệ. Tôi luôn sẵn lòng tham gia vào các cơ hội mới và thách thức, cùng với việc mở rộng mạng lưới kết nối chuyên nghiệp của mình. Cảm ơn đã dành thời gian tìm hiểu về tôi!**


# Cookies

GitBook cung cấp mức độ minh bạch cao về cách chúng tôi sử dụng dữ liệu của bạn, cách chúng tôi thu thập dữ liệu của bạn và chia sẻ dữ liệu của bạn với ai. Để làm được điều đó, chúng tôi cung cấp trang này, chi tiết về các bên xử lý phụ của chúng tôi, cách chúng tôi sử dụng cookie và nơi cũng như cách thức chúng tôi thực hiện việc theo dõi nào đó trên GitBook.

## Bộ xử lý phụ GitBook

{% content-ref url="/pages/YVM8ft3KJfp1WZj4GP68" %}
[Subprocessors](/cookies/subprocessors)
{% endcontent-ref %}

## Cookie trên GitBook

Các bên xử lý phụ của GitBook TRANG Các bên xử lý phụ Cookie trên GitBook GitBook sử dụng cookie để làm cho việc tương tác với dịch vụ của chúng tôi trở nên dễ dàng và có ý nghĩa. Chúng tôi sử dụng cookie (và các công nghệ tương tự, như HTML5 localStorage) để giữ bạn đăng nhập, nhớ các tùy chọn của bạn và cung cấp thông tin cho sự phát triển của GitBook trong tương lai.

Cookie là một mẩu văn bản nhỏ mà máy chủ web của chúng tôi lưu trữ trên máy tính hoặc thiết bị di động của bạn, và trình duyệt của bạn gửi lại cho chúng tôi khi bạn quay trở lại trang web của chúng tôi. Cookie không nhất thiết xác định bạn nếu bạn chỉ đơn thuần là khách truy cập GitBook; tuy nhiên, một cookie có thể lưu trữ một mã nhận dạng duy nhất cho mỗi người dùng đã đăng nhập. Cookie mà GitBook đặt là cần thiết cho việc hoạt động của website, hoặc được sử dụng cho hiệu suất hoặc chức năng. Bằng cách sử dụng trang web của chúng tôi, bạn đồng ý rằng chúng tôi có thể đặt các loại cookie này trên máy tính hoặc thiết bị của bạn. Nếu bạn vô hiệu hóa khả năng chấp nhận cookie của trình duyệt hoặc thiết bị của bạn, bạn sẽ không thể đăng nhập hoặc sử dụng các dịch vụ của GitBook.

GitBook đặt các cookie sau đây trên người dùng của chúng tôi với các lý do sau:

<table data-full-width="false"><thead><tr><th>Tên Cookie</th><th>Lý do</th></tr></thead><tbody><tr><td><code>:__session</code></td><td>Cookie này được sử dụng để đăng nhập bạn.</td></tr><tr><td><code>:_ga</code></td><td>Cookie này được sử dụng bởi Google Analytics.</td></tr><tr><td><code>:_stripe</code></td><td>Cookie này được sử dụng bởi Stripe.</td></tr></tbody></table>

Một số trang trên trang web của chúng tôi có thể đặt các cookie của bên thứ ba khác. Ví dụ, chúng tôi có thể nhúng nội dung, như video, từ một trang khác đặt cookie. Mặc dù chúng tôi cố gắng giảm thiểu các cookie của bên thứ ba này, chúng tôi không thể luôn kiểm soát cookie nào mà nội dung của bên thứ ba này đặt.

## Theo dõi trên GitBook

Theo dõi trên GitBook "Do Not Track" là một lựa chọn quyền riêng tư bạn có thể đặt trong trình duyệt của mình nếu bạn không muốn các dịch vụ trực tuyến - cụ thể là mạng quảng cáo - thu thập và chia sẻ một số loại thông tin về hoạt động trực tuyến của bạn từ các dịch vụ theo dõi của bên thứ ba. Hiện tại GitBook không phản hồi khác biệt đối với cài đặt Do Not Track của một trình duyệt cá nhân. Nếu bạn muốn thiết lập trình duyệt của mình để tín hiệu rằng bạn không muốn bị theo dõi, vui lòng kiểm tra tài liệu của trình duyệt của bạn để bật tín hiệu đó. Cũng có các ứng dụng tốt ngăn chặn việc theo dõi trực tuyến, như Privacy Badger.

Chúng tôi không theo dõi hoạt động duyệt web trực tuyến của bạn trên các dịch vụ trực tuyến khác theo thời gian và chúng tôi không chủ nhà cho quảng cáo của bên thứ ba trên GitBook có thể theo dõi hoạt động của bạn trên trang web của chúng tôi. Chúng tôi có thỏa thuận với một số nhà cung cấp, như các nhà cung cấp phân tích, giúp chúng tôi theo dõi việc di chuyển của khách truy cập trên một số trang trên trang web của chúng tôi. Chỉ những nhà cung cấp của chúng tôi, những người đang thu thập dữ liệu thay mặt cho chúng tôi, mới có thể thu thập dữ liệu trên các trang của chúng tôi, và chúng tôi đã ký các thỏa thuận bảo vệ dữ liệu với mọi nhà cung cấp thu thập dữ liệu này thay mặt cho chúng tôi. Chúng tôi sử dụng dữ liệu chúng tôi nhận được từ những nhà cung cấp này để hiểu rõ hơn về sở thích của khách truy cập, hiểu về hiệu suất của trang web của chúng tôi và cải thiện nội dung của mình. Bất kỳ nhà cung cấp phân tích nào cũng sẽ được liệt kê trong Danh sách Bên xử lý phụ của chúng tôi ở trên, và bạn có thể xem danh sách mọi trang nơi chúng tôi thu thập loại dữ liệu này dưới đây.

## **Google Analytics**

Google Analytics Chúng tôi sử dụng Google Analytics như một dịch vụ phân tích của bên thứ ba, nhưng chúng tôi không sử dụng nó cho mục đích quảng cáo. Chúng tôi sử dụng Google Analytics để thu thập thông tin về cách trang web của chúng tôi hoạt động và cách người dùng của chúng tôi, nói chung, điều hướng và sử dụng GitBook. Điều này giúp chúng tôi đánh giá việc sử dụng GitBook của người dùng; biên soạn các báo cáo thống kê về hoạt động; và cải thiện nội dung và hiệu suất trang web của chúng tôi. Google cung cấp thêm thông tin về các thực hành riêng tư của mình và cung cấp một tiện ích mở rộng trình duyệt để từ chối theo dõi Google Analytics.

## Các trang trên GitBook nơi phân tích được bật

Các trang trên GitBook nơi phân tích được kích hoạt Phân tích hoặc mã theo dõi khác được kích hoạt trên trang web chính của chúng tôi ([www.gitbook.com](http://www.gitbook.com)) và các tên miền được phục vụ và lưu trữ bởi GitBook (`*.gitbook.io` và tên miền tùy chỉnh). Nếu bạn muốn ngăn chúng tôi thu thập thông tin về hoạt động duyệt web của bạn trên GitBook, bạn có thể sử dụng một trình chặn theo dõi như Privacy Badger hoặc chọn không tham gia theo dõi của Google Analytics.


# Subprocessors

Khi chúng tôi chia sẻ thông tin của bạn với các bên phụ xử lý, như các nhà cung cấp và dịch vụ của chúng tôi, chúng tôi vẫn chịu trách nhiệm về nó. Chúng tôi làm việc rất chăm chỉ để duy trì niềm tin của bạn khi chúng tôi thuê thêm nhà cung cấp mới, và chúng tôi yêu cầu tất cả các nhà cung cấp phải ký kết các thỏa thuận bảo vệ dữ liệu với chúng tôi, giới hạn việc họ xử lý Thông Tin Cá Nhân của Người Dùng (như được định nghĩa trong Bản Tuyên bố Quyền riêng tư).

<table data-full-width="false"><thead><tr><th>Tên Bên Phụ Xử Lý</th><th>Mô Tả Xử Lý</th><th>Vị Trí Xử Lý</th></tr></thead><tbody><tr><td>Google Cloud / Firebase</td><td>Hosting provider</td><td>United States</td></tr><tr><td>Stripe</td><td>Subscription credit card payment processor</td><td>United States</td></tr><tr><td>DocuSign</td><td>Contract signature processor</td><td>United States</td></tr><tr><td>Google Apps</td><td>Internal company infrastructure</td><td>United States</td></tr><tr><td>Google Analytics</td><td>Website analytics and performance</td><td>United States</td></tr><tr><td>HelpScout</td><td>Customer support ticketing system</td><td>United States</td></tr><tr><td>Sentry</td><td>Error analytics processor</td><td>United States</td></tr><tr><td>Amplitude</td><td>Customer analytics processor</td><td>United States</td></tr><tr><td>Segment</td><td>Customer analytics processor</td><td>United States</td></tr><tr><td>Castle.io</td><td>Security &#x26; Bots detection</td><td>United States</td></tr><tr><td>LaunchDarkly</td><td>Feature flagging</td><td>United States</td></tr><tr><td>Iframely</td><td>Embeds generation</td><td>United States</td></tr><tr><td>WorkOS</td><td>SSO &#x26; SCIM infrastructure</td><td>United States</td></tr><tr><td>Sendgrid</td><td>Email infrastructure</td><td>United States</td></tr><tr><td>Algolia</td><td>Search infrastructure</td><td>United States</td></tr><tr><td>MagicBell</td><td>Notifications infrastructure</td><td>United States</td></tr><tr><td>OpenAI</td><td>AI features for our search engine</td><td>United States</td></tr><tr><td>Intercom</td><td>Support tool connecting our customers</td><td>United States</td></tr></tbody></table>

Khi chúng tôi tuyển dụng một nhà cung cấp mới hoặc bên phụ xử lý khác có thao tác với Thông Tin Cá Nhân của Người Dùng, hoặc loại bỏ một bên phụ xử lý, hoặc chúng tôi thay đổi cách sử dụng một bên phụ xử lý, chúng tôi sẽ cập nhật trang này.


# Welcome

Welcome to the GitBook starter template! Here you'll get an overview of all the amazing features GitBook offers to help you build beautiful, interactive documentation.

You'll see some of the best parts of GitBook in action — and find help on how you can turn this template into your own.

### Jump right in

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><h4><i class="fa-bolt">:bolt:</i></h4></td><td><strong>Quickstart</strong></td><td>Create your first site</td><td></td><td></td><td><a href="/ai-agentic/getting-started/quickstart">Quickstart</a></td></tr><tr><td><h4><i class="fa-leaf">:leaf:</i></h4></td><td><strong>Editor basics</strong></td><td>Learn the basics of GitBook</td><td></td><td></td><td><a href="https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md">https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md</a></td></tr><tr><td><h4><i class="fa-globe-pointer">:globe-pointer:</i></h4></td><td><strong>Publish your docs</strong></td><td>Share your docs online</td><td></td><td></td><td><a href="/ai-agentic/getting-started/publish-your-docs">Publish your docs</a></td></tr></tbody></table>


# Quickstart

<figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-hero.png" alt=""><figcaption></figcaption></figure>

Beautiful documentation starts with the content you create — and GitBook makes it easy to get started with any pre-existing content.

{% hint style="info" %}
Want to learn about writing content from scratch? Head to the [Basics](broken://pages/i73g4LZQanoLj7XtSO18) section to learn more.
{% endhint %}

### Import

GitBook supports importing content from many popular writing tools and formats. If your content already exists, you can upload a file or group of files to be imported.

<div data-full-width="false"><figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-import.png" alt=""><figcaption></figcaption></figure></div>

### Sync a repository

GitBook also allows you to set up a bi-directional sync with an existing repository on GitHub or GitLab. Setting up Git Sync allows you and your team to write content in GitBook or in code, and never have to worry about your content becoming out of sync.


# Publish your docs

Once you’ve finished writing, editing, or importing your content, you can publish your work to the web as a docs site. Once published, your site will be accessible online only to your selected audience.

You can publish your site and find related settings from your docs site's homepage.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/publish-hero.png" alt=""><figcaption></figcaption></figure>


# Overview

Các nền tảng này đều mô tả Agent Skills như một chuẩn chung để mở rộng khả năng của AI agents.

## Tài liệu hướng dẫn Agent Skills

### Tài liệu tham khảo chính

Các tài liệu chính thức về Agent Skills:

1. OpenAI Codex Skills\
   <https://developers.openai.com/codex/skills/>
2. OpenCode Skills Documentation\
   <https://opencode.ai/docs/skills/>
3. Claude Agent Skills Documentation\
   <https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview>
4. Các chuyên gia AI sẵn sàng chuyển đổi quy trình làm việc của bạn\
   <https://github.com/msitarzewski/agency-agents>

***

## 1. Agent Skills là gì

**Agent Skills** là các module mở rộng cho AI agent, giúp agent có thể thực hiện những nhiệm vụ chuyên biệt.

Skill thường bao gồm:

* Instructions (hướng dẫn)
* Scripts (code thực thi)
* Resources (tài liệu tham khảo)

Mục tiêu:

```
Write once → reuse everywhere
```

Một skill giúp agent thực hiện **một workflow cụ thể** thay vì phải dùng prompt dài lặp lại nhiều lần.

***

## 2. Vì sao cần Agent Skills

Agent Skills giúp:

#### 1. Chuyên môn hóa AI

Ví dụ:

| Không có skill | Có skill                |
| -------------- | ----------------------- |
| AI chung chung | AI chuyên code React    |
| AI chung chung | AI chuyên deploy Docker |
| AI chung chung | AI chuyên phân tích log |

***

#### 2. Tái sử dụng workflow

Thay vì prompt dài:

```
Write a changelog
Generate release notes
Create GitHub release command
```

Chỉ cần:

```
use skill: git-release
```

***

#### 3. Tự động load khi cần

Agent có thể:

* tự chọn skill
* hoặc gọi skill trực tiếp

Ví dụ:

```
skill({ name: "git-release" })
```

Agent sẽ load nội dung SKILL.md vào context.

***

## 3. Cấu trúc của một Skill

Một skill là **một thư mục**.

Ví dụ:

```
my-skill/
│
├── SKILL.md
├── scripts/
│   └── run.py
├── references/
│   └── api-doc.md
└── assets/
    └── template.md
```

Thành phần chính:

| File       | Chức năng     |
| ---------- | ------------- |
| SKILL.md   | bắt buộc      |
| scripts    | code thực thi |
| references | tài liệu      |
| assets     | template      |

***

## 4. File SKILL.md

File quan trọng nhất.

Bắt buộc có **YAML frontmatter**.

Ví dụ:

```
---
name: git-release
description: Create consistent releases and changelogs
---
```

Quy tắc:

* name: 1–64 ký tự
* chỉ chữ thường + số + dấu "-"
* description: 1–1024 ký tự

***

## 5. Nội dung SKILL.md

Ví dụ:

```
# Git Release Skill

## What I do

- Generate release notes
- Suggest version bump
- Create GitHub release command

## When to use

Use this when preparing a new release.
```

***

## 6. Cách agent tìm và load skill

Agent sẽ scan các thư mục:

#### OpenCode

```
.opencode/skills/
~/.config/opencode/skills/
.claude/skills/
~/.claude/skills/
.agents/skills/
```

Agent sẽ tìm:

```
skills/<skill-name>/SKILL.md
```

***

## 7. Cách cài skill

Ví dụ với Codex:

```
~/.codex/skills/
```

Hoặc:

```
project/.codex/skills/
```

Sau khi cài:

```
restart agent
```

Agent sẽ tự động phát hiện skill mới.

***

## 8. Cách agent sử dụng skill

Có 2 cách.

#### 1. Auto detect

User prompt:

```
Create a GitHub release
```

Agent sẽ chọn skill:

```
git-release
```

***

#### 2. Manual call

```
skill({ name: "git-release" })
```

***

## 9. Permission của skill

Trong file config:

```
opencode.json
```

Ví dụ:

```
{
  "permission": {
    "skill": {
      "*": "allow",
      "internal-*": "deny",
      "experimental-*": "ask"
    }
  }
}
```

Các trạng thái:

| Permission | Meaning              |
| ---------- | -------------------- |
| allow      | load ngay            |
| deny       | không cho agent thấy |
| ask        | hỏi user             |

***

## 10. Disable skills

Có thể tắt hoàn toàn skill tool:

```
tools:
  skill: false
```

Agent sẽ không sử dụng skill.

***

## 11. Progressive Loading (quan trọng)

Skill được load theo từng bước:

#### Level 1

Metadata

```
name
description
```

***

#### Level 2

Instructions

```
SKILL.md
```

***

#### Level 3

Resources / scripts

```
scripts/
references/
```

Cách này giúp:

```
tiết kiệm context
```

***

## 12. Agent Skills là open standard

Hiện nay nhiều agent hỗ trợ cùng một chuẩn skill:

* Claude Code
* OpenAI Codex
* Cursor
* VSCode AI agents
* OpenCode

Các skill có thể **chạy cross-platform** giữa nhiều agent.

***

## 13. Ví dụ skill thực tế

### React Expert Skill

```
name: react-expert
description: Help build React applications
```

Chức năng:

* generate component
* optimize hooks
* create project structure

***

### DevOps Skill

```
name: docker-deploy
description: Deploy Docker services
```

Chức năng:

* build Dockerfile
* create compose
* deploy container

***

## 14. Lưu ý bảo mật

Skill có thể:

* chạy code
* đọc file
* gọi API

Nếu skill độc hại có thể:

* đánh cắp dữ liệu
* chạy lệnh nguy hiểm

Vì vậy chỉ nên dùng skill từ **nguồn đáng tin cậy**.

***

## Tóm tắt

Agent Skills là **plugin cho AI agent**.

Skill gồm:

```
instructions
scripts
resources
```

Cấu trúc:

```
skill/
 ├ SKILL.md
 ├ scripts/
 ├ references/
 └ assets/
```

Skill giúp:

```
AI chuyên môn hóa
workflow tái sử dụng
automation mạnh hơn
```

***

## Template chuẩn tạo Agent Skill

### 1. Cấu trúc thư mục

```
skills/
└── your-skill-name/
    ├── SKILL.md
    ├── README.md
    ├── examples/
    │   └── example.md
    ├── scripts/
    │   └── run.py
    ├── resources/
    │   └── reference.md
    └── templates/
        └── template.md
```

Ý nghĩa:

| Folder    | Chức năng                      |
| --------- | ------------------------------ |
| SKILL.md  | file chính để agent hiểu skill |
| README.md | tài liệu cho developer         |
| examples  | ví dụ cách dùng                |
| scripts   | code thực thi                  |
| resources | tài liệu tham khảo             |
| templates | template output                |

***

### 2. File bắt buộc: SKILL.md

Đây là file quan trọng nhất.

### Template

````markdown
---
name: your-skill-name
description: Short description of what the skill does and when the agent should use it
version: 1.0.0
author: your-name
tags:
  - automation
  - coding
  - ai-agent
---

# Your Skill Name

## Purpose

Explain what this skill does.

Example:

This skill helps the AI agent generate release notes and GitHub releases automatically.

---

## When to Use

Use this skill when:

- The user asks to create releases
- The user asks for changelog generation
- The user prepares a software release

Do NOT use this skill when:

- The user is only asking about Git concepts
- The user is debugging Git issues

---

## Instructions

Follow these steps:

1. Analyze the user's request
2. Determine the required workflow
3. Use available scripts if necessary
4. Produce the final output in a structured format

---

## Workflow

Typical workflow:

1. Gather information
2. Process the data
3. Generate structured output

Example:

User request → analyze → generate release notes → output markdown.

---

## Output Format

Always return results in this format:

```bash
## Result

Summary:
...

Details:
...

Next Steps:
...

```

---

## Available Resources

The following resources may help:

resources/reference.md

---

## Scripts

If execution is needed use scripts from:

scripts/

Example:

```bash
python scripts/run.py
```

---

## Examples

Example user request:

```bash
Create release notes for version 2.0
```

Example output:

```bash
## Release Notes

### Features

- New API

#### Fixes

- Bug fixes

```

````

***

### 3. README.md (cho developer)

Template:

```
# Skill: your-skill-name

## Description

Explain what the skill does.

## Directory Structure

SKILL.md – main skill instructions  
scripts/ – executable scripts  
resources/ – reference materials  
examples/ – usage examples  

## Installation

Copy folder into:

.claude/skills/
.codex/skills/
.opencode/skills/

## Usage

The agent will automatically detect the skill when relevant.

Or manually call:

skill("your-skill-name")
```

***

### 4. Template script (scripts/run.py)

```
import sys

def main():
    print("Skill script executed successfully")

if __name__ == "__main__":
    main()
```

Agent chỉ nhận **output của script**, không cần load code vào context.

***

### 5. Template resource file

`resources/reference.md`

```
# Reference Guide

Important information used by the skill.

Example:

API endpoints
data schema
workflow rules
```

***

### 6. Template example

`examples/example.md`

```
User request:

Generate release notes for version 1.2

Expected output:

# Release Notes

## Features
- Feature A

## Fixes
- Fix B
```

***

### 7. Quy tắc đặt tên Skill

| Rule              | Example       |
| ----------------- | ------------- |
| lowercase only    | good          |
| use hyphen        | docker-deploy |
| max 64 characters | recommended   |

Không dùng:

```
spaces
UPPERCASE
special characters
```

***

### 8. Ví dụ Skill hoàn chỉnh

```
skills/
└── docker-deploy/
    ├── SKILL.md
    ├── scripts/
    │   └── deploy.py
    └── resources/
        └── docker-reference.md
```

Skill này có thể:

* build docker image
* generate docker-compose
* deploy container

***

### 9. Best Practices

#### Viết description rõ ràng

Tốt:

```
Generate structured blog posts from topic outlines
```

Kém:

```
Blog tool
```

***

#### Hạn chế prompt dài

Skill nên:

```
instructions < 5000 tokens
```

***

#### Dùng script cho logic phức tạp

Không nên:

```
AI tự viết code mỗi lần
```

Nên:

```
AI gọi script
```

***

### 10. Template nhanh nhất (minimal skill)

Nếu cần **skill đơn giản**, chỉ cần:

```
your-skill/
└── SKILL.md
```

Ví dụ:

```
---
name: blog-writer
description: Generate blog posts from topic prompts
---

# Blog Writer Skill

## Instructions

When the user asks for a blog post:

1. Generate outline
2. Write structured article
3. Include headings and sections
```

***


# Tại sao nên cân nhắc xây dựng Skills trước thay vì Agents?

{% code title="Tài liệu tham khảo" overflow="wrap" lineNumbers="true" %}

```markdown
- https://viblo.asia/p/agent-skills-tai-sao-nen-can-nhac-xay-dung-skills-truoc-thay-vi-agents-pPLkNBAeJRZ
```

{% endcode %}


# Hướng dẫn cài đặt và đăng nhập Codex trên Ubuntu

### 1. Giới thiệu

**OpenAI Codex** là công cụ AI hỗ trợ lập trình, có khả năng:

* viết code
* chỉnh sửa code
* giải thích project
* tự động hóa một số tác vụ lập trình

Codex có thể chạy trực tiếp trong terminal thông qua **CLI (Command Line Interface)**.

Bài viết này hướng dẫn:

* Cài đặt Codex CLI trên Ubuntu
* Đăng nhập và sử dụng Codex

***

## 2. Chuẩn bị môi trường

Codex CLI yêu cầu **Node.js phiên bản 16 trở lên**.

### Kiểm tra Node.js

Mở terminal và chạy:

```
node -v
```

Nếu hệ thống đã cài Node.js, bạn sẽ thấy ví dụ:

```
v18.17.0
```

Nếu chưa có, tiến hành cài đặt.

***

## 3. Cài đặt Node.js và npm

Chạy các lệnh sau:

```
sudo apt update
sudo apt install nodejs npm -y
```

Sau khi cài xong, kiểm tra lại:

```
node -v
npm -v
```

Ví dụ kết quả:

```
node v18.x
npm 9.x
```

***

## 4. Cài đặt Codex CLI

Sau khi có Node.js, cài Codex bằng **npm**:

```
npm install -g @openai/codex
```

Tùy tốc độ mạng, quá trình này mất khoảng **30–60 giây**.

***

## 5. Kiểm tra cài đặt

Sau khi cài xong, kiểm tra:

```
codex --version
```

Nếu hiển thị version nghĩa là cài đặt thành công.

Bạn cũng có thể kiểm tra vị trí chương trình:

```
which codex
```

Ví dụ:

```
/usr/bin/codex
```

***

## 6. Đăng nhập Codex

Để sử dụng Codex, bạn cần đăng nhập tài khoản.

Chạy lệnh:

```
codex
```

Sau đó:

1. Chọn **Sign in with ChatGPT**
2. Trình duyệt sẽ mở trang đăng nhập
3. Đăng nhập tài khoản ChatGPT
4. Xác nhận cấp quyền cho Codex

Sau khi xác thực xong, terminal sẽ hiển thị trạng thái **login thành công**.

***

## 7. Sử dụng Codex

Di chuyển vào thư mục project:

```
cd myproject
```

Sau đó chạy:

```
codex
```

Bạn có thể nhập các yêu cầu như:

```
Explain this project
```

hoặc

```
Create a REST API with Node.js
```

Codex sẽ phân tích project và đề xuất code.

***

## 8. Cập nhật Codex

Để cập nhật Codex lên phiên bản mới:

```
codex --upgrade
```

***

## 9. Một số lệnh Codex hữu ích

| Lệnh              | Chức năng                 |
| ----------------- | ------------------------- |
| codex             | mở Codex interactive      |
| codex --auto-edit | cho phép sửa code tự động |
| codex --upgrade   | cập nhật phiên bản        |
| codex --version   | xem phiên bản             |

***

## 10. Lưu ý khi dùng trên server

Nếu bạn cài Codex trên **Ubuntu Server qua SSH**, đôi khi trình duyệt không mở được.

Bạn có thể đăng nhập bằng:

```
codex login
```

Sau đó:

* copy link đăng nhập
* mở link trên máy local
* xác thực tài khoản

***

## 11. Kết luận

Sau khi hoàn thành các bước trên, bạn đã:

* cài đặt Codex CLI
* đăng nhập tài khoản
* sử dụng Codex trong project

Codex giúp tăng tốc quá trình lập trình và tự động hóa nhiều công việc trong phát triển phần mềm.


# Hướng dẫn cài đặt Claude trên Ubuntu

Bài viết này hướng dẫn cách cài đặt và sử dụng **Claude** trên hệ điều hành **Ubuntu** thông qua công cụ dòng lệnh **Claude Code** do **Anthropic** phát triển.

Claude Code cho phép bạn:

* Chat với Claude trực tiếp trong terminal
* Hỗ trợ viết code, sửa lỗi, tạo file
* Tích hợp vào workflow lập trình

***

## 1. Yêu cầu hệ thống

Trước khi cài đặt cần đảm bảo:

* Ubuntu **20.04 trở lên**
* RAM tối thiểu **4GB**
* Có Internet
* Terminal (bash/zsh)

Nếu cài bằng npm thì cần thêm:

* **Node.js 18 trở lên**

Kiểm tra Node.js:

```
node -v
```

***

## 2. Cách 1 (Khuyến nghị) – Cài đặt bằng script chính thức

Cách này **không cần Node.js**.

Chạy lệnh:

```
curl -fsSL https://claude.ai/install.sh | bash
```

Sau khi cài xong, chương trình sẽ được đặt tại:

```
~/.local/bin/claude
```

Cách này cài bản binary độc lập và tự cập nhật dễ dàng.

***

## 3. Cách 2 – Cài bằng npm

Nếu bạn đã có Node.js:

```
npm install -g @anthropic-ai/claude-code
```

⚠️ Không nên dùng:

```
sudo npm install -g
```

vì có thể gây lỗi quyền truy cập.

***

## 4. Kiểm tra cài đặt

Sau khi cài xong, kiểm tra:

```
claude --version
```

Hoặc:

```
claude doctor
```

Nếu hiển thị phiên bản nghĩa là cài đặt thành công.

***

## 5. Đăng nhập Claude

Chạy:

```
claude
```

Hệ thống sẽ mở trình duyệt để đăng nhập tài khoản Claude.

Có thể đăng nhập bằng:

* tài khoản **Claude Pro / Max**
* hoặc API từ **Anthropic Console**

Sau khi xác thực xong, CLI sẽ tự lưu thông tin đăng nhập.

***

## 6. Sử dụng Claude trong terminal

Ví dụ:

```
claude
```

Sau đó nhập câu hỏi:

```
Explain Docker networking
```

Hoặc trong thư mục project:

```
cd my-project
claude
```

Claude sẽ đọc code và hỗ trợ chỉnh sửa.

***

## 7. Một số lệnh hữu ích

#### Cập nhật Claude

```
claude update
```

#### Tắt auto update

```
claude config set autoUpdates false --global
```

#### Kiểm tra hệ thống

```
claude doctor
```

***

## 8. Gỡ cài đặt

Nếu cài bằng npm:

```
npm uninstall -g @anthropic-ai/claude-code
```

Nếu cài bằng script:

```
rm ~/.local/bin/claude
```

***

## 9. Mẹo sử dụng

Claude CLI rất mạnh khi dùng với:

* Git repository
* Docker project
* DevOps script
* Tự động sửa code

Bạn có thể yêu cầu:

```
Refactor this code
Write a Dockerfile
Explain this error
```

***

✅ Sau khi cài xong, bạn có thể dùng Claude trực tiếp trong terminal để hỗ trợ lập trình, DevOps và tự động hóa workflow.


# Hướng dẫn cài đặt GitHub Copilot trên Ubuntu

Bài viết này hướng dẫn cách cài đặt và sử dụng **GitHub Copilot** trên hệ điều hành **Ubuntu** thông qua **Visual Studio Code**.

GitHub Copilot là trợ lý AI giúp:

* Tự động gợi ý code
* Hoàn thành hàm
* Viết comment và documentation
* Hỗ trợ nhiều ngôn ngữ lập trình

***

## 1. Yêu cầu hệ thống

Trước khi cài đặt cần đảm bảo:

* Ubuntu **20.04 trở lên**
* Có tài khoản **GitHub**
* Cài **Visual Studio Code**

Kiểm tra Ubuntu:

```
lsb_release -a
```

***

## 2. Cài đặt Visual Studio Code

Nếu máy chưa có **Visual Studio Code**, chạy:

```
sudo apt update
sudo apt install wget gpg
```

Thêm repository:

```
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
```

Thêm repo VS Code:

```
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
```

Cài đặt:

```
sudo apt update
sudo apt install code
```

Kiểm tra:

```
code --version
```

***

## 3. Cài GitHub Copilot Extension

Mở **Visual Studio Code**

Nhấn:

```
Ctrl + Shift + X
```

Tìm extension:

```
GitHub Copilot
```

Cài extension:

👉 **GitHub Copilot Extension**

Hoặc cài bằng terminal:

```
code --install-extension GitHub.copilot
```

***

## 4. Đăng nhập GitHub

Sau khi cài extension:

1. VS Code sẽ yêu cầu **Sign in to GitHub**
2. Trình duyệt mở ra
3. Đăng nhập tài khoản **GitHub**
4. Cho phép Copilot truy cập

Sau khi xác nhận, Copilot sẽ hoạt động trong VS Code.

***

## 5. Kiểm tra Copilot hoạt động

Tạo file ví dụ:

```
hello.py
```

Viết comment:

```
# function to calculate fibonacci
```

Copilot sẽ tự gợi ý code.

Nhấn:

```
Tab
```

để chấp nhận gợi ý.

***

## 6. Các phím tắt hữu ích

| Phím     | Chức năng       |
| -------- | --------------- |
| Tab      | chấp nhận gợi ý |
| Esc      | bỏ gợi ý        |
| Alt + ]  | gợi ý tiếp theo |
| Alt + \[ | gợi ý trước     |

***

## 7. Cài GitHub Copilot Chat (tuỳ chọn)

Extension này cho phép **chat trực tiếp với AI trong VS Code**.

Cài:

```
GitHub Copilot Chat
```

Hoặc terminal:

```
code --install-extension GitHub.copilot-chat
```

Sau đó mở chat:

```
Ctrl + Alt + I
```

***

## 8. Một số mẹo sử dụng

Copilot hoạt động tốt khi bạn:

Viết comment rõ ràng:

```
# create a REST API using Flask
```

Hoặc:

```
// function to validate email address
```

Copilot sẽ tạo code phù hợp.

***

## 9. Kiểm tra trạng thái Copilot

Trong VS Code:

```
Ctrl + Shift + P
```

Gõ:

```
Copilot: Status
```

***

## 10. Gỡ cài đặt

Nếu muốn gỡ:

```
code --uninstall-extension GitHub.copilot
```

***

## Kết luận

Sau khi cài đặt **GitHub Copilot**, bạn có thể:

* Viết code nhanh hơn
* Tự động hoàn thành hàm
* Học cách viết code chuẩn

Copilot đặc biệt hữu ích cho:

* Python
* JavaScript
* Docker
* DevOps
* Backend development


# Hướng dẫn cài đặt OpenCode trên Ubuntu

Bài viết này hướng dẫn cách cài đặt **OpenCode** trên hệ điều hành **Ubuntu** để sử dụng AI coding agent trực tiếp trong terminal.

OpenCode là một **AI coding agent mã nguồn mở** giúp bạn:

* Viết và chỉnh sửa code bằng AI
* Phân tích project code
* Chạy lệnh terminal tự động
* Tích hợp nhiều nhà cung cấp LLM

Dự án được phát triển và công khai trên **OpenCode**.

***

## 1. Yêu cầu hệ thống

Trước khi cài đặt cần đảm bảo:

* Ubuntu **20.04 trở lên**
* Terminal hiện đại (Kitty, Alacritty, WezTerm…)
* Internet
* API key của các nhà cung cấp LLM (OpenAI, Anthropic, v.v.)

***

## 2. Cài đặt nhanh (khuyến nghị)

Cách đơn giản nhất để cài OpenCode là dùng **install script chính thức**.

Chạy lệnh:

```
curl -fsSL https://opencode.ai/install | bash
```

Script sẽ:

* tải binary mới nhất
* cài vào thư mục người dùng
* tự thêm vào PATH

Phương pháp này hoạt động trên **Linux, macOS và WSL**.

***

## 3. Kiểm tra cài đặt

Sau khi cài xong, kiểm tra:

```
opencode --version
```

Hoặc:

```
opencode
```

Nếu giao diện terminal của OpenCode xuất hiện nghĩa là cài đặt thành công.

***

## 4. Cài đặt bằng Node.js (tuỳ chọn)

Nếu máy bạn đã có **Node.js**, có thể cài bằng npm:

```
npm install -g opencode-ai
```

Ngoài npm còn hỗ trợ:

```
pnpm install -g opencode-ai
yarn global add opencode-ai
bun install -g opencode-ai
```

Các package manager này đều cài **OpenCode CLI toàn cục**.

***

## 5. Cài đặt bằng Homebrew (Linux)

Nếu sử dụng **Homebrew trên Linux**:

```
brew install anomalyco/tap/opencode
```

Repository này thường có **phiên bản mới nhất** của OpenCode.

***

## 6. Chạy OpenCode

Sau khi cài đặt:

```
opencode
```

OpenCode sẽ mở **TUI (Terminal UI)** để bạn chat với AI và làm việc với project code.

Ví dụ:

```
Explain this repository
Refactor this code
Create a Dockerfile
```

***

## 7. Các agent trong OpenCode

OpenCode có sẵn **2 agent chính**:

#### build

* Agent mặc định
* Có quyền chỉnh sửa file
* Chạy lệnh bash

#### plan

* Agent chỉ đọc
* Phân tích code
* Không sửa file nếu chưa được phép

Bạn có thể chuyển agent bằng phím:

```
Tab
```

***

## 8. Một số lệnh CLI hữu ích

#### Xem provider đã đăng nhập

```
opencode auth list
```

#### Đăng xuất provider

```
opencode auth logout
```

#### Chạy web interface

```
opencode web
```

Lệnh này sẽ mở **OpenCode web UI** trên trình duyệt.

***

## 9. Gỡ cài đặt OpenCode

Để gỡ OpenCode:

```
opencode uninstall
```

Lệnh này sẽ xóa binary và các file cấu hình liên quan.

***

## 10. Mẹo sử dụng OpenCode

OpenCode hoạt động rất tốt với:

* Git repository
* Docker project
* DevOps script
* Backend development

Ví dụ prompt hiệu quả:

```
Explain this project structure
Fix this Python error
Create a Dockerfile for this app
Write unit tests
```

***

✅ Sau khi cài xong, bạn có thể sử dụng **OpenCode trực tiếp trong terminal để viết code bằng AI**, tương tự như Claude Code hoặc Cursor CLI.

{% code title="Tài liệu tham khảo" overflow="wrap" lineNumbers="true" %}

```markdown
- https://opencode.ai/
- https://github.com/anomalyco/opencode
- https://opencode.ai/docs
```

{% endcode %}


# About


# Giới thiệu n8n-nodes-nqdev

Giới thiệu n8n-nodes-nqdev: Tự động hóa quy trình công việc với các dịch vụ SaaS

Trong thế giới hiện đại, tự động hóa quy trình công việc (workflow automation) không chỉ giúp tiết kiệm thời gian mà còn nâng cao hiệu suất công việc. Một trong những công cụ phổ biến giúp thực hiện điều này là **n8n**, một nền tảng mã nguồn mở cho phép kết nối và tự động hóa các dịch vụ và ứng dụng khác nhau mà không cần phải lập trình phức tạp.

Nếu bạn đang sử dụng các dịch vụ của **nqdev-group** như **eSMSvn**, **Haravan**, hoặc **Zalo**, thì bộ **n8n-nodes-nqdev** sẽ là một trợ thủ đắc lực. Đây là bộ sưu tập các **custom n8n nodes** được phát triển để tích hợp những dịch vụ SaaS này vào trong quy trình tự động hóa của bạn, giúp bạn dễ dàng kết nối và xử lý dữ liệu giữa các nền tảng này.

## Các Tính Năng Nổi Bật Của **n8n-nodes-nqdev**

### **1. eSMSvn: Tự Động Gửi SMS Marketing**

eSMSvn là dịch vụ SMS marketing phổ biến tại Việt Nam, giúp bạn dễ dàng gửi tin nhắn marketing, thông báo, hoặc chăm sóc khách hàng qua SMS. Với **n8n-nodes-nqdev**, bạn có thể tích hợp eSMSvn vào quy trình tự động của mình, ví dụ như gửi tin nhắn tự động khi có đơn hàng mới hoặc thông báo sự kiện.

### **2. Haravan: Tự Động Quản Lý Thương Mại Điện Tử**

Haravan là nền tảng giúp bạn xây dựng và quản lý cửa hàng trực tuyến. Với các node của **n8n-nodes-nqdev**, bạn có thể tự động hóa nhiều tác vụ trong hệ thống như đồng bộ đơn hàng, quản lý sản phẩm, hoặc tự động gửi email thông báo cho khách hàng về tình trạng đơn hàng.

### **3. Zalo: Giao Tiếp Và Quản Lý Khách Hàng**

Zalo không chỉ là mạng xã hội mà còn là một công cụ mạnh mẽ để tương tác với khách hàng qua các tính năng như Official Account, Zalo API, và chatbot. **n8n-nodes-nqdev** giúp bạn tích hợp Zalo vào quy trình tự động hóa, ví dụ như gửi thông báo qua Zalo hoặc trả lời tự động cho khách hàng qua Zalo chatbot.

## Cách Cài Đặt **`n8n-nodes-nqdev`**

Để sử dụng các node này trong n8n, bạn cần làm theo các bước sau:

1. **Cài Đặt n8n**: Trước tiên, nếu bạn chưa cài đặt n8n, bạn cần cài đặt nó. Bạn có thể tham khảo hướng dẫn cài đặt trên trang chính thức của n8n.
2. **Cài Đặt n8n-nodes-nqdev**: Sau khi n8n đã được cài đặt, bạn cần tải và cài đặt bộ node này từ GitHub:
   * Truy cập repo [n8n-nodes-nqdev trên GitHub](https://github.com/nqdev-group/n8n-nodes-nqdev).
   * Tải về hoặc clone repo này vào thư mục plugin của n8n.
3. **Cấu Hình Các Dịch Vụ**: Sau khi cài đặt, bạn cần cấu hình các node tương ứng với các dịch vụ như eSMSvn, Haravan và Zalo trong n8n. Điều này bao gồm việc nhập các thông tin API và cấu hình các tham số cho các node.
4. **Tạo Workflow**: Cuối cùng, bạn có thể tạo các workflow tự động hóa trong n8n, kết nối các node này để tự động hóa các tác vụ như gửi tin nhắn, quản lý đơn hàng, hay giao tiếp với khách hàng.

## Ví Dụ Sử Dụng

Giả sử bạn muốn tự động gửi một tin nhắn qua eSMSvn mỗi khi có một đơn hàng mới được tạo trên Haravan. Bạn có thể tạo một workflow trong n8n như sau:

1. **Trigger**: Sử dụng node "Haravan" để theo dõi khi có đơn hàng mới.
2. **Action**: Sử dụng node "eSMSvn" để gửi một tin nhắn SMS thông báo tới khách hàng hoặc đội ngũ của bạn.

## Lợi Ích Của Việc Sử Dụng **n8n-nodes-nqdev**

* **Tiết Kiệm Thời Gian**: Tự động hóa các tác vụ lặp lại giúp bạn tiết kiệm thời gian và công sức, cho phép bạn tập trung vào các công việc quan trọng hơn.
* **Giảm Thiểu Lỗi**: Việc tự động hóa giúp giảm thiểu khả năng xảy ra lỗi do thao tác thủ công.
* **Tăng Hiệu Quả**: Tăng cường hiệu quả công việc bằng cách kết nối các dịch vụ với nhau và tự động xử lý thông tin, giảm thiểu sự chậm trễ và cải thiện trải nghiệm người dùng.

## Kết Luận

**n8n-nodes-nqdev** là một bộ công cụ mạnh mẽ giúp kết nối và tự động hóa các dịch vụ của nqdev-group vào n8n, tạo ra những quy trình làm việc thông minh và hiệu quả. Dù bạn là chủ doanh nghiệp, nhà phát triển hay người quản lý, việc sử dụng bộ node này sẽ giúp bạn tối ưu hóa quy trình công việc, giảm thiểu công sức thủ công và nâng cao trải nghiệm khách hàng.

Hãy bắt đầu tự động hóa công việc của bạn ngay hôm nay với **n8n-nodes-nqdev**!


# Hướng dẫn cài đặt và sử dụng n8n-nodes-nqdev

Nếu bạn đang sử dụng các dịch vụ SaaS của **nqdev-group** như **eSMSvn**, **Haravan**, hoặc **Zalo**, việc tích hợp chúng vào n8n để tự động hóa quy trình công việc sẽ giúp bạn tiết kiệm thời gian và tăng cường hiệu suất công việc. Bộ **n8n-nodes-nqdev** cung cấp các node tùy chỉnh giúp bạn dễ dàng tích hợp và sử dụng các dịch vụ này trong các workflow tự động của n8n.

Dưới đây là hướng dẫn chi tiết về cách **cài đặt và sử dụng** **n8n-nodes-nqdev**.

## **1. Cài Đặt n8n**

Trước tiên, bạn cần cài đặt **n8n** nếu chưa có. N8n là công cụ mã nguồn mở cho phép bạn tạo và tự động hóa các quy trình làm việc giữa các dịch vụ và ứng dụng.

### **Cài Đặt n8n với Docker (Cách đơn giản nhất)**

1. **Cài đặt Docker**:
   * Tải và cài đặt Docker nếu bạn chưa có.
2. **Chạy n8n với Docker**:

   * Mở terminal và chạy lệnh sau để tạo và chạy container Docker cho n8n:

   ```bash
   docker run -it --rm \
     -p 5678:5678 \
     --name n8n \
     n8nio/n8n
   ```

   Sau khi Docker khởi động, n8n sẽ chạy trên `http://localhost:5678`.

### **Cài Đặt n8n với npm (Dành cho những ai muốn cài trực tiếp)**

1. **Cài đặt Node.js và npm**:
   * Tải và cài đặt [Node.js](https://nodejs.org/) (phiên bản LTS) và npm (npm thường đi kèm với Node.js).
2. **Cài đặt n8n**:

   * Chạy lệnh sau để cài đặt n8n:

   ```bash
   npm install n8n -g
   ```
3. **Khởi động n8n**:

   * Sau khi cài đặt xong, chạy lệnh sau để khởi động n8n:

   ```bash
   n8n
   ```

   Truy cập vào n8n qua trình duyệt tại `http://localhost:5678`.

***

## 2. Cài Đặt **n8n-nodes-nqdev**

Để cài đặt **n8n-nodes-nqdev**, bạn cần tải bộ node này từ GitHub và cài đặt vào n8n của bạn.

### **Bước 1: Clone Repo từ GitHub**

1. Truy cập repo **n8n-nodes-nqdev** trên GitHub:\
   <https://github.com/nqdev-group/n8n-nodes-nqdev>
2. Clone repo vào thư mục plugin của n8n:

   ```bash
   git clone https://github.com/nqdev-group/n8n-nodes-nqdev.git ~/.n8n/nodes/n8n-nodes-nqdev
   ```

   Nếu bạn chưa có thư mục `~/.n8n/nodes`, bạn có thể tạo thư mục đó trước:

   ```bash
   mkdir -p ~/.n8n/nodes
   ```

### **Bước 2: Cài Đặt Các Phụ Thuộc**

1. Truy cập vào thư mục **n8n-nodes-nqdev**:

   ```bash
   cd ~/.n8n/nodes/n8n-nodes-nqdev
   ```
2. Cài đặt các dependencies cần thiết:

   ```bash
   npm install
   ```

### **Bước 3: Khởi Động Lại n8n**

Sau khi cài đặt xong, bạn cần khởi động lại n8n để n8n nhận diện các node mới:

```bash
n8n restart
```

***

## **3. Cấu Hình Các Node eSMSvn, Haravan và Zalo**

Các node trong **n8n-nodes-nqdev** cho phép bạn tích hợp dễ dàng các dịch vụ của **nqdev-group** như eSMSvn, Haravan, và Zalo vào trong các workflow của n8n. Để sử dụng các node này, bạn cần cấu hình API key hoặc thông tin cần thiết cho từng dịch vụ.

### **Cấu Hình Node eSMSvn**

1. **Tạo API Key**: Truy cập trang quản lý của **eSMSvn** và lấy API Key từ phần quản lý tài khoản.
2. **Cấu Hình Node**: Trong n8n, tạo một node **eSMSvn** và nhập API Key vào trường cấu hình.

### **Cấu Hình Node Haravan**

1. **Tạo API Key**: Truy cập vào trang quản trị của **Haravan** và tạo API Key cho cửa hàng của bạn.
2. **Cấu Hình Node**: Thêm node **Haravan** vào workflow và nhập API Key cùng thông tin xác thực cần thiết.

### **Cấu Hình Node Zalo**

1. **Tạo Official Account**: Tạo một Official Account trên **Zalo** và lấy API Key.
2. **Cấu Hình Node**: Trong n8n, thêm node **Zalo** vào workflow và nhập thông tin API Key, Access Token cần thiết.

***

## **4. Tạo Workflow Tự Động Hóa Với n8n-nodes-nqdev**

Sau khi đã cài đặt và cấu hình các node, bạn có thể bắt đầu tạo các workflow tự động hóa giữa các dịch vụ. Dưới đây là một ví dụ đơn giản về cách sử dụng **n8n-nodes-nqdev** để tự động gửi SMS khi có đơn hàng mới trên Haravan.

### **Ví Dụ Workflow: Gửi SMS Khi Có Đơn Hàng Mới**

1. **Node Trigger**: Sử dụng node **Haravan** để theo dõi sự kiện tạo đơn hàng mới.
2. **Node Action**: Sử dụng node **eSMSvn** để gửi một tin nhắn SMS cho khách hàng khi đơn hàng được tạo.

### **Các bước trong n8n:**

* Thêm node **Haravan** vào workflow và cấu hình để theo dõi đơn hàng mới.
* Thêm node **eSMSvn** và cấu hình để gửi tin nhắn SMS thông qua API của eSMSvn.
* Kết nối hai node lại với nhau để khi đơn hàng mới được tạo trên Haravan, một tin nhắn SMS sẽ được gửi tự động.

***

## **5. Kiểm Tra và Chạy Workflow**

Khi bạn đã cấu hình đầy đủ các node và kết nối chúng lại, bạn có thể kiểm tra workflow của mình bằng cách nhấn vào nút **Execute Workflow** trong giao diện n8n.

Nếu mọi thứ hoạt động bình thường, bạn sẽ thấy quá trình tự động hóa diễn ra, chẳng hạn như SMS sẽ được gửi khi có đơn hàng mới từ Haravan.

***

## **Kết Luận**

Bộ **n8n-nodes-nqdev** là một công cụ mạnh mẽ để tích hợp các dịch vụ của **nqdev-group** vào n8n, giúp bạn dễ dàng tự động hóa các tác vụ như gửi SMS, quản lý đơn hàng, và tương tác với khách hàng qua Zalo. Bằng cách làm theo các bước trên, bạn có thể nhanh chóng cài đặt và sử dụng các node này để tối ưu hóa quy trình công việc của mình.

Hãy thử ngay hôm nay và tận dụng sức mạnh của tự động hóa!


# Giới thiệu n8n-nodes-nqdev-esmsvn


# Giới thiệu n8n-nodes-nqdev-haravan


# Welcome

Welcome to the GitBook starter template! Here you'll get an overview of all the amazing features GitBook offers to help you build beautiful, interactive documentation.

You'll see some of the best parts of GitBook in action — and find help on how you can turn this template into your own.

### Jump right in

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><h4><i class="fa-bolt">:bolt:</i></h4></td><td><strong>Quickstart</strong></td><td>Create your first site</td><td></td><td></td><td><a href="/build-server/getting-started/quickstart">Quickstart</a></td></tr><tr><td><h4><i class="fa-leaf">:leaf:</i></h4></td><td><strong>Editor basics</strong></td><td>Learn the basics of GitBook</td><td></td><td></td><td><a href="https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md">https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md</a></td></tr><tr><td><h4><i class="fa-globe-pointer">:globe-pointer:</i></h4></td><td><strong>Publish your docs</strong></td><td>Share your docs online</td><td></td><td></td><td><a href="/build-server/getting-started/publish-your-docs">Publish your docs</a></td></tr></tbody></table>


# Quickstart

<figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-hero.png" alt=""><figcaption></figcaption></figure>

Beautiful documentation starts with the content you create — and GitBook makes it easy to get started with any pre-existing content.

{% hint style="info" %}
Want to learn about writing content from scratch? Head to the [Basics](broken://pages/i73g4LZQanoLj7XtSO18) section to learn more.
{% endhint %}

### Import

GitBook supports importing content from many popular writing tools and formats. If your content already exists, you can upload a file or group of files to be imported.

<div data-full-width="false"><figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-import.png" alt=""><figcaption></figcaption></figure></div>

### Sync a repository

GitBook also allows you to set up a bi-directional sync with an existing repository on GitHub or GitLab. Setting up Git Sync allows you and your team to write content in GitBook or in code, and never have to worry about your content becoming out of sync.


# Publish your docs

Once you’ve finished writing, editing, or importing your content, you can publish your work to the web as a docs site. Once published, your site will be accessible online only to your selected audience.

You can publish your site and find related settings from your docs site's homepage.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/publish-hero.png" alt=""><figcaption></figcaption></figure>


# Cài đặt ứng dụng trên Ubuntu Server

Đây là các phương pháp phổ biến nhất:

### 1. Sử dụng APT (Package Manager mặc định)

bash Copy

```bash
# Cập nhật danh sách gói
sudo apt update

# Cài đặt ứng dụng (ví dụ: nginx)
sudo apt install nginx

# Cài đặt nhiều gói cùng lúc
sudo apt install package1 package2 package3

# Cài đặt phiên bản cụ thể
sudo apt install nginx=1.18.0-0ubuntu1
```

### 2. Sử dụng Snap

bash Copy

```bash
# Tìm kiếm ứng dụng
snap find firefox

# Cài đặt
sudo snap install firefox

# Cài đặt với channel cụ thể (stable/candidate/beta/edge)
sudo snap install node --channel=18/stable

# Cài đặt classic confinement
sudo snap install code --classic
```

### 3. Sử dụng Docker

bash Copy

```bash
# Cài đặt Docker trước
sudo apt install docker.io

# Chạy container
sudo docker run -d -p 80:80 nginx

# Hoặc sử dụng Docker Compose
# Tạo file docker-compose.yml rồi chạy:
sudo docker-compose up -d
```

### 4. Cài đặt từ Source Code

bash Copy

```bash
# Ví dụ: Cài đặt từ GitHub
git clone https://github.com/user/repo.git
cd repo

# Build từ source (tùy thuộc vào ngôn ngữ)
# Node.js:
npm install && npm run build

# Python:
pip install -r requirements.txt

# Go:
go build

# C/C++:
./configure && make && sudo make install
```

### 5. Cài đặt file .deb

bash Copy

```bash
# Tải file .deb về
wget https://example.com/app.deb

# Cài đặt bằng dpkg
sudo dpkg -i app.deb

# Fix dependencies nếu có lỗi
sudo apt --fix-broken install
```

### 6. Cài đặt từ PPA (Personal Package Archive)

bash Copy

```bash
# Thêm PPA repository
sudo add-apt-repository ppa:nginx/stable

# Cập nhật và cài đặt
sudo apt update
sudo apt install nginx
```

### Các lệnh quản lý hữu ích

Table

| Lệnh                      | Mô tả                   |
| ------------------------- | ----------------------- |
| `sudo apt remove package` | Gỡ cài đặt              |
| `sudo apt purge package`  | Gỡ cài đặt + xóa config |
| `sudo apt autoremove`     | Xóa dependencies thừa   |
| `apt search keyword`      | Tìm kiếm gói            |
| `apt show package`        | Xem thông tin gói       |
| `dpkg -l \| grep package` | Kiểm tra đã cài chưa    |

### Ví dụ thực tế: Cài đặt LAMP Stack

bash Copy

```bash
# Update hệ thống
sudo apt update && sudo apt upgrade -y

# Cài Apache
sudo apt install apache2 -y

# Cài MySQL
sudo apt install mysql-server -y
sudo mysql_secure_installation

# Cài PHP
sudo apt install php libapache2-mod-php php-mysql -y

# Restart Apache
sudo systemctl restart apache2
```

### Lưu ý quan trọng

1. **Luôn update trước khi cài**: `sudo apt update`
2. **Kiểm tra port**: Đảm bảo port cần thiết đã mở (`sudo ufw allow 80/tcp`)
3. **Service auto-start**: `sudo systemctl enable servicename`
4. **Log files**: Kiểm tra `/var/log/` nếu có lỗi

Bạn muốn cài đặt ứng dụng cụ thể nào? Tôi có thể hướng dẫn chi tiết hơn.


# Kiểm tra Firewall đang cài đặt

#### 1. Kiểm tra UFW (Uncomplicated Firewall)

bash Copy

```bash
# Kiểm tra UFW đã cài chưa
dpkg -l | grep ufw

# Kiểm tra trạng thái UFW
sudo ufw status
sudo ufw status verbose
sudo ufw status numbered

# Kiểm tra cấu hình chi tiết
sudo ufw show raw
cat /etc/default/ufw
```

#### 2. Kiểm tra iptables

bash Copy

```bash
# Kiểm tra iptables đã cài chưa
dpkg -l | grep iptables

# Kiểm tra rules hiện tại
sudo iptables -L -v -n
sudo iptables -L -v -n --line-numbers

# Kiểm tra NAT table
sudo iptables -t nat -L -v -n

# Kiểm tra mangle table
sudo iptables -t mangle -L -v -n
```

#### 3. Kiểm tra nftables (thay thế iptables trên Ubuntu mới)

bash Copy

```bash
# Kiểm tra nftables
dpkg -l | grep nftables

# Kiểm tra rules
sudo nft list ruleset

# Kiểm tra tables
sudo nft list tables

# Kiểm tra chi tiết từng table
sudo nft list table inet filter
```

#### 4. Kiểm tra Firewalld

bash Copy

```bash
# Kiểm tra firewalld
dpkg -l | grep firewalld

# Kiểm tra trạng thái
sudo systemctl status firewalld

# Liệt kê zones và rules
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all
sudo firewall-cmd --list-all-zones
```

### Script kiểm tra tổng hợp

bash Copy

```bash
#!/bin/bash
echo "=== KIỂM TRA FIREWALL TRÊN UBUNTU SERVER ==="
echo ""

echo "1. UFW (Uncomplicated Firewall):"
if dpkg -l | grep -q ufw; then
    echo "   ✅ Đã cài đặt"
    echo "   Trạng thái: $(sudo ufw status | head -1)"
    sudo ufw status | grep -E "(Status|ALLOW|DENY|REJECT)" | head -10
else
    echo "   ❌ Chưa cài đặt"
fi
echo ""

echo "2. iptables:"
if dpkg -l | grep -q iptables; then
    echo "   ✅ Đã cài đặt"
    echo "   Số rules: $(sudo iptables -L | grep -c "Chain")"
    sudo iptables -L INPUT -v -n | head -5
else
    echo "   ❌ Chưa cài đặt"
fi
echo ""

echo "3. nftables:"
if dpkg -l | grep -q nftables; then
    echo "   ✅ Đã cài đặt"
    sudo nft list tables 2>/dev/null || echo "   Không có tables"
else
    echo "   ❌ Chưa cài đặt"
fi
echo ""

echo "4. Firewalld:"
if dpkg -l | grep -q firewalld; then
    echo "   ✅ Đã cài đặt"
    sudo systemctl is-active firewalld 2>/dev/null || echo "   Không active"
else
    echo "   ❌ Chưa cài đặt"
fi
echo ""

echo "5. Các port đang mở (listening):"
sudo ss -tlnp | grep LISTEN
echo ""

echo "6. Kiểm tra xung đột:"
if systemctl is-active --quiet ufw && systemctl is-active --quiet firewalld 2>/dev/null; then
    echo "   ⚠️ CẢNH BÁO: Cả UFW và Firewalld đang chạy!"
elif systemctl is-active --quiet ufw; then
    echo "   ✓ Chỉ UFW đang chạy"
elif systemctl is-active --quiet firewalld 2>/dev/null; then
    echo "   ✓ Chỉ Firewalld đang chạy"
else
    echo "   ℹ️ Không có firewall nào đang chạy (chỉ có iptables/nftables kernel)"
fi
```

### Kiểm tra chi tiết cấu hình

#### Kiểm tra backend của UFW

bash Copy

```bash
# UFW sử dụng iptables hay nftables?
cat /etc/default/ufw | grep IPT_BACKEND

# Kiểm tra profile UFW
ls /etc/ufw/applications.d/
cat /etc/ufw/applications.d/openssh-server

# Kiểm tra before/after rules
cat /etc/ufw/before.rules
cat /etc/ufw/after.rules
```

#### Kiểm tra iptables persistent

bash Copy

```bash
# Kiểm tra iptables-persistent
dpkg -l | grep persistent

# Kiểm tra rules đã lưu
ls /etc/iptables/
cat /etc/iptables/rules.v4 2>/dev/null
cat /etc/iptables/rules.v6 2>/dev/null
```

#### Kiểm tra với netstat/ss

bash Copy

```bash
# Liệt kê tất cả kết nối
sudo ss -tulnp

# Chỉ xem port đang mở
sudo ss -tlnp | grep -v "127.0.0.1"

# Kiểm tra theo protocol
sudo ss -tlnp  # TCP
sudo ss -ulnp  # UDP
```

### Kiểm tra xung đột và vấn đề

bash Copy

```bash
# Kiểm tra nhiều firewall cùng chạy
sudo systemctl list-units --type=service | grep -E "(ufw|firewalld|nftables|iptables)"

# Kiểm tra log firewall
sudo dmesg | grep -i firewall
sudo tail -f /var/log/ufw.log

# Kiểm tra policy mặc định
sudo iptables -L | grep -i policy
```

### Bảng so sánh Firewall trên Ubuntu

Table

| Firewall      | Mức độ     | Mặc định Ubuntu | Giao diện    |
| ------------- | ---------- | --------------- | ------------ |
| **UFW**       | Đơn giản   | ✅ Có            | CLI đơn giản |
| **iptables**  | Trung bình | Kernel built-in | CLI phức tạp |
| **nftables**  | Trung bình | Ubuntu 22.04+   | CLI mới      |
| **Firewalld** | Nâng cao   | ❌ Không         | CLI + GUI    |

### Lệnh kiểm tra nhanh

bash Copy

```bash
# Tóm tắt trong 1 lệnh
echo "=== FIREWALL STATUS ===" && \
sudo ufw status 2>/dev/null && \
echo "" && echo "=== IPTABLES RULES ===" && \
sudo iptables -L -n | head -10 && \
echo "" && echo "=== OPEN PORTS ===" && \
sudo ss -tlnp
```

Kết quả kiểm tra cho thấy firewall nào đang hoạt động trên server của bạn?


# install-common

## Clean UP Ubuntu Linux

🧹 Lệnh cơ bản dọn rác (chạy theo thứ tự)

```bash
# 1. Cập nhật danh sách gói
sudo apt update

# 2. Xóa gói không cần thiết (autoremove)
sudo apt autoremove -y

# 3. Xóa cấu hình thừa của gói đã gỡ
sudo apt purge $(dpkg -l | awk '/^rc/ {print $2}') -y

# 4. Dọn cache APT (2 lựa chọn)
sudo apt clean
sudo apt-get clean
sudo apt autoclean

# 🗂️ Dọn các loại rác khác
# 5. Dọn log hệ thống (journald)
# Giới hạn log 7 ngày
sudo journalctl --vacuum-time=7d

# Hoặc giới hạn 500MB
sudo journalctl --vacuum-size=500M

# 6. Dọn file tạm
# Xóa file tạm trong /tmp (cẩn thận!)
sudo find /tmp -type f -mtime +7 -delete

# Xóa cache người dùng
rm -rf ~/.cache/thumbnails/*

# 7. Xóa kernel cũ (giữ lại kernel đang chạy)
# Kiểm tra kernel đang dùng (KHÔNG xóa!)
uname -r

# Xóa kernel cũ tự động
sudo apt autoremove --purge -y

# 8. Dọn Snap cache
# Xóa phiên bản snap cũ
snap list --all | awk '/disabled/{print $1, $3}' | while read snapname revision; do
    sudo snap remove "$snapname" --revision="$revision"
done
```

***

## Unikey Ubuntu Linux

### Cách 1: Cài đặt IBus-Unikey (Khuyến nghị)

#### Bước 1: Cài đặt gói ibus-unikey

```bash
sudo apt update
sudo apt install ibus-unikey -y
```

#### Bước 2: Khởi động lại IBus

```bash
ibus restart
```

[Nếu gặp lỗi "Can't connect to IBus", hãy đăng xuất và đăng nhập lại Ubuntu.](https://gist.github.com/codedeep79/cd473b24559342872311ad99fedaf551)

#### Bước 3: Cấu hình Input Method

Trên Ubuntu 22.04 và mới hơn:

1. Mở Settings → Keyboard → Input Sources
2. Nhấn dấu + để thêm input source
3. Tìm Vietnamese → chọn Vietnamese (Unikey) → nhấn Add

#### Bước 4: Cấu hình Language Support (nếu cần)

1. Tìm Language Support trong menu
2. Ở mục Keyboard input method system, chuyển sang IBus (nếu chưa phải IBus)
3. Khởi động lại máy

#### Bước 5: Sử dụng

* Nhấn **Super + Space** (hoặc **Ctrl + Space**) để chuyển đổi giữa tiếng Anh và tiếng Việt
* Biểu tượng **en/vi** sẽ xuất hiện ở góc trên bên phải màn hình


# install-docker-compsoe

## Docker & Docker Compose

Để cài đặt Docker và Docker Compose trên Ubuntu Server, bạn có thể làm theo các bước sau:

### Bước 1: Cập nhật hệ thống

Trước tiên, hãy cập nhật danh sách gói của hệ thống để đảm bảo tất cả các gói đang được cài đặt là phiên bản mới nhất.

```bash
sudo apt update
sudo apt upgrade -y
```

### Bước 2: Cài đặt Docker

Docker có thể được cài đặt bằng cách sử dụng kho chính thức của Docker.

1. Cài đặt các gói phụ trợ cần thiết:

   ```bash
   sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
   ```
2. Thêm kho lưu trữ của Docker vào hệ thống:

   ```bash
   curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
   ```
3. Thêm kho Docker vào nguồn gói của apt:

   ```bash
   sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
   ```
4. Cập nhật lại danh sách gói:

   ```bash
   sudo apt update
   ```
5. Cài đặt Docker:

   ```bash
   sudo apt install docker-ce -y
   ```
6. Kiểm tra xem Docker có hoạt động không:

   ```bash
   sudo systemctl status docker
   ```

   Nếu Docker đang chạy, bạn sẽ thấy thông báo "active (running)".

### Bước 3: Thêm người dùng vào nhóm `docker`

Để tránh việc phải sử dụng `sudo` mỗi khi chạy lệnh Docker, bạn có thể thêm người dùng của mình vào nhóm `docker`.

```bash
sudo usermod -aG docker $USER

sudo usermod -aG docker minipc
```

Sau đó, đăng xuất và đăng nhập lại để thay đổi có hiệu lực.

### Bước 4: Cài đặt Docker Compose

Docker Compose là một công cụ giúp dễ dàng quản lý các container Docker bằng cách sử dụng tệp cấu hình YAML.

1. Tải và cài đặt Docker Compose:

   ```bash
   sudo curl -L "https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name)/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
   ```
2. Cấp quyền thực thi cho tệp docker-compose:

   ```bash
   sudo chmod +x /usr/local/bin/docker-compose
   ```
3. Kiểm tra cài đặt Docker Compose:

   ```bash
   docker-compose --version
   ```

   Bạn sẽ thấy thông tin phiên bản Docker Compose nếu mọi thứ đã được cài đặt đúng.

### Bước 5: Cấu hình Docker để tự động khởi động cùng hệ thống

Để Docker tự động khởi động khi hệ thống khởi động lại, bạn có thể bật chế độ tự khởi động:

```bash
sudo systemctl enable docker
```

### Bước 6: Kiểm tra lại Docker và Docker Compose

1. Chạy một container đơn giản để kiểm tra Docker:

   ```bash
   docker run hello-world
   ```
2. Tạo một tệp `docker-compose.yml` và thử chạy một dịch vụ với Docker Compose:

   ```yml
   version: "3"
   services:
     web:
       image: nginx
       ports:
         - "80:80"
   ```

   Chạy lệnh sau để khởi động dịch vụ:

   ```bash
   docker-compose up
   ```

***


# 00\_move\_swap.sh

## Hướng dẫn chuyển Swap từ `/swap.img` sang ổ khác trên Ubuntu

Trong một số trường hợp, ổ hệ thống (`/`) bị giới hạn dung lượng hoặc muốn tận dụng ổ đĩa khác có dung lượng lớn hơn, chúng ta có thể **di chuyển swap sang ổ khác**.

Ví dụ trong bài này:

* Swap cũ: `/swap.img`
* Swap mới: `/mnt/e02/uenv/swapfile`

Bài viết hướng dẫn cách **chuyển swap an toàn và tự động bằng script Bash**.

***

## 1. Kiểm tra swap hiện tại

Trước tiên kiểm tra swap đang dùng:

```
swapon --show
```

Hoặc:

```
free -h
```

Ví dụ kết quả:

```
NAME       TYPE SIZE USED PRIO
/swap.img  file 2G   0B   -2
```

***

## 2. Script Bash tự động chuyển swap

Script dưới đây sẽ thực hiện toàn bộ quá trình:

* Tắt swap cũ
* Tạo swap mới
* Format swap
* Bật swap mới
* Cập nhật `/etc/fstab`
* Xóa swap cũ

***

### Tạo file script

```
nano move_swap.sh
```

Dán nội dung sau:

```
#!/bin/bash

set -e

OLD_SWAP="/swap.img"
NEW_SWAP="/mnt/e02/uenv/swapfile"
SWAP_SIZE="8G"

echo "=== Bat dau chuyen swap ==="

echo "1. Tat swap cu..."
sudo swapoff $OLD_SWAP || true

echo "2. Tao swap moi tai $NEW_SWAP ..."
sudo mkdir -p /mnt/e02/uenv

sudo fallocate -l $SWAP_SIZE $NEW_SWAP || \
sudo dd if=/dev/zero of=$NEW_SWAP bs=1M count=8192

echo "3. Set permission..."
sudo chmod 600 $NEW_SWAP

echo "4. Format swap..."
sudo mkswap $NEW_SWAP

echo "5. Bat swap moi..."
sudo swapon $NEW_SWAP

echo "6. Cap nhat /etc/fstab..."

sudo sed -i '\|/swap.img|d' /etc/fstab

if ! grep -q "$NEW_SWAP" /etc/fstab; then
    echo "$NEW_SWAP none swap sw 0 0" | sudo tee -a /etc/fstab
fi

echo "7. Xoa swap cu..."
if [ -f "$OLD_SWAP" ]; then
    sudo rm -f $OLD_SWAP
fi

echo "=== Hoan tat ==="
echo "Kiem tra swap:"

swapon --show
free -h
```

***

## 3. Cấp quyền chạy script

```
chmod +x move_swap.sh
```

***

## 4. Chạy script

```
./move_swap.sh
```

Sau khi chạy xong, swap sẽ được chuyển sang:

```
/mnt/e02/uenv/swapfile
```

***

## 5. Kiểm tra kết quả

```
swapon --show
```

Ví dụ kết quả:

```
NAME                      TYPE SIZE USED PRIO
/mnt/e02/uenv/swapfile    file 8G   0B   -2
```

Kiểm tra RAM và swap:

```
free -h
```

***

## 6. Tối ưu hệ thống (khuyến nghị)

Giảm việc Linux dùng swap quá nhiều:

```
sudo sysctl vm.swappiness=10
```

Lưu cấu hình vĩnh viễn:

```
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
```

***

## 7. Một số lưu ý

#### Không nên dùng `mv /swap.img`

Vì swap đang được hệ thống sử dụng.

#### SSD vs HDD

* SSD → swap hoạt động tốt
* HDD → swap sẽ chậm

#### Dung lượng swap gợi ý

| RAM   | Swap khuyến nghị |
| ----- | ---------------- |
| 4GB   | 4-8GB            |
| 8GB   | 8-16GB           |
| 16GB  | 8GB              |
| 32GB+ | 4-8GB            |

***

## 8. Kiểm tra sau khi reboot

Sau khi reboot máy:

```
swapon --show
```

Nếu thấy:

```
/mnt/e02/uenv/swapfile
```

→ cấu hình thành công.

***

✅ Bạn có thể lưu bài này thành:

```
README_swap_move.md
```

hoặc

```
docs/linux/move_swap_ubuntu.md
```

***

Nếu bạn muốn, tôi có thể viết thêm **bài nâng cao rất hay**:

* **Auto tạo swap theo RAM (RAM × 1 hoặc RAM × 2)**
* **Script tự phát hiện ổ disk lớn nhất để đặt swap**
* **Tối ưu swap cho server AI / Docker / n8n** (rất hữu ích với máy bạn đang chạy).

{% code title="00\_move\_swap.sh" overflow="wrap" lineNumbers="true" %}

```
#!/bin/bash

set -e

OLD_SWAP="/swap.img"
NEW_SWAP="/mnt/e02/uenv/swapfile"
SWAP_SIZE="8G"

echo "=== Bat dau chuyen swap ==="

echo "1. Tat swap cu..."
sudo swapoff $OLD_SWAP || true

echo "2. Tao swap moi tai $NEW_SWAP ..."
sudo mkdir -p /mnt/e02/uenv

sudo fallocate -l $SWAP_SIZE $NEW_SWAP || \
sudo dd if=/dev/zero of=$NEW_SWAP bs=1M count=8192

echo "3. Set permission..."
sudo chmod 600 $NEW_SWAP

echo "4. Format swap..."
sudo mkswap $NEW_SWAP

echo "5. Bat swap moi..."
sudo swapon $NEW_SWAP

echo "6. Cap nhat /etc/fstab..."

sudo sed -i '\|/swap.img|d' /etc/fstab

if ! grep -q "$NEW_SWAP" /etc/fstab; then
    echo "$NEW_SWAP none swap sw 0 0" | sudo tee -a /etc/fstab
fi

echo "7. Xoa swap cu..."
if [ -f "$OLD_SWAP" ]; then
    sudo rm -f $OLD_SWAP
fi

echo "=== Hoan tat ==="
echo "Kiem tra swap:"

swapon --show
free -h
```

{% endcode %}


# install-fail2ban

## Install Fail2ban

#### 1. Cài đặt Fail2ban

**Cập nhật hệ thống**

bash Copy

```bash
sudo apt update && sudo apt upgrade -y
```

**Cài đặt Fail2ban**

bash Copy

```bash
sudo apt install fail2ban -y
```

**Kiểm tra phiên bản**

bash Copy

```bash
fail2ban-client --version
```

Theo LinuxCapable , Fail2ban có sẵn trong repository mặc định của Ubuntu.

***

#### 2. Khởi động và kích hoạt dịch vụ

bash Copy

```bash
# Kích hoạt và khởi động ngay lập tức
sudo systemctl enable fail2ban --now

# Hoặc tách riêng lệnh
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
```

**Kiểm tra trạng thái**

bash Copy

```bash
sudo systemctl status fail2ban
```

Nếu thấy `Active: active (running)` là thành công .

***

#### 3. Cấu hình Fail2ban

**Nguyên tắc quan trọng**

**KHÔNG** sửa file `jail.conf` trực tiếp vì sẽ bị ghi đè khi cập nhật. Thay vào đó, tạo file `jail.local` .

**Tạo file cấu hình local**

bash Copy

```bash
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
```

**Cấu hình cơ bản (thêm vào file `jail.local`)**

ini Copy

```ini
[DEFAULT]
# Danh sách IP được bỏ qua (không bị ban)
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 YOUR_IP_ADDRESS

# Thời gian ban (10 phút = 600 giây, -1 = vĩnh viễn)
bantime = 10m

# Thời gian theo dõi (10 phút)
findtime = 10m

# Số lần thử sai tối đa
maxretry = 5

# Backend firewall (tự động phát hiện hoặc chọn iptables/ufw/nftables)
banaction = iptables-multiport

# Gửi email thông báo (tùy chọn)
destemail = admin@yourdomain.com
sender = fail2ban@yourdomain.com
action = %(action_mw)s

# Tăng thời gian ban cho tái phạm
bantime.increment = true
bantime.factor = 2
bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 1h
findtime = 10m
```

**Giải thích tham số** :

* `ignoreip`: IP không bị ban (thêm IP của bạn vào đây)
* `bantime`: Thời gian khóa IP
* `findtime`: Khung thời gian đếm số lần thất bại
* `maxretry`: Số lần thử sai tối đa trước khi bị ban

***

#### 4. Tích hợp với UFW (Ubuntu Firewall)

Nếu dùng UFW, thay đổi `banaction` :ini Copy

```ini
[DEFAULT]
banaction = ufw
```

Hoặc cho jail SSH cụ thể:ini Copy

```ini
[sshd]
banaction = ufw
```

***

#### 5. Khởi động lại và kiểm tra

**Áp dụng cấu hình mới**

bash Copy

```bash
sudo systemctl restart fail2ban
```

**Kiểm tra cấu hình**

bash Copy

```bash
sudo fail2ban-client -t
```

**Xem trạng thái jail SSH**

bash Copy

```bash
sudo fail2ban-client status sshd
```

Kết quả mẫu :plain Copy

```
Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed: 0
|  `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
   |- Currently banned: 0
   |- Total banned: 0
   `- Banned IP list:
```

***

#### 6. Quản lý Fail2ban

**Kiểm tra tất cả jail đang chạy**

bash Copy

```bash
sudo fail2ban-client status
```

**Ban IP thủ công**

bash Copy

```bash
sudo fail2ban-client set sshd banip 192.168.1.100
```

**Gỡ ban IP**

bash Copy

```bash
sudo fail2ban-client set sshd unbanip 192.168.1.100
```

**Xem log thời gian thực**

bash Copy

```bash
sudo tail -f /var/log/fail2ban.log
```

***

#### 7. Cấu hình nâng cao

**Bảo vệ thêm dịch vụ khác**

Thêm vào `jail.local` :ini Copy

```ini
# Bảo vệ Apache
[apache-auth]
enabled = true
port = http,https
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 3

# Bảo vệ Nginx
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3

# Bảo vệ FTP (ProFTPD)
[proftpd]
enabled = true
port = ftp,ftp-data
filter = proftpd
logpath = /var/log/proftpd/proftpd.log
maxretry = 5

# Bảo vệ Mail (Postfix)
[postfix]
enabled = true
port = smtp,465,submission
filter = postfix
logpath = /var/log/mail.log
maxretry = 3
```

**Cấu hình ban vĩnh viễn**

ini Copy

```ini
bantime = -1
```

***

#### 8. Script tự động cài đặt

Tạo file `install_fail2ban.sh`:bash Copy

```bash
#!/bin/bash

echo "=== CÀI ĐẶT FAIL2BAN CHO UBUNTU SERVER ==="

# Cập nhật hệ thống
sudo apt update && sudo apt upgrade -y

# Cài đặt Fail2ban
sudo apt install fail2ban -y

# Sao lưu file gốc nếu chưa có
if [ ! -f /etc/fail2ban/jail.local ]; then
    sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
fi

# Cấu hình cơ bản
sudo tee /etc/fail2ban/jail.d/defaults-ubuntu.conf > /dev/null <<EOF
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
bantime = 10m
findtime = 10m
maxretry = 5
banaction = ufw

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 1h
EOF

# Khởi động dịch vụ
sudo systemctl enable fail2ban --now

# Kiểm tra trạng thái
echo ""
echo "=== KIỂM TRA TRẠNG THÁI ==="
sudo systemctl status fail2ban --no-pager

echo ""
echo "=== CẤU HÌNH SSH JAIL ==="
sudo fail2ban-client status sshd

echo ""
echo "✅ Cài đặt Fail2ban hoàn tất!"
echo "📋 Xem log: sudo tail -f /var/log/fail2ban.log"
echo "📊 Xem status: sudo fail2ban-client status sshd"
```

Chạy script:bash Copy

```bash
chmod +x install_fail2ban.sh
sudo ./install_fail2ban.sh
```

***

#### ⚠️ Lưu ý quan trọng

Table

| Vấn đề                  | Giải pháp                                              |
| ----------------------- | ------------------------------------------------------ |
| **Tự khóa mình**        | Thêm IP của bạn vào `ignoreip` trước khi kích hoạt     |
| **Không ban được**      | Kiểm tra `backend` phù hợp với hệ thống (systemd/file) |
| **UFW không hoạt động** | Đổi `banaction` thành `ufw`                            |
| **Ubuntu 24.04+**       | Dùng `nftables` backend thay vì `iptables`             |

***

#### Kiểm tra phiên bản Ubuntu

Table

| Ubuntu    | Fail2ban | Backend mặc định   |
| --------- | -------- | ------------------ |
| 22.04 LTS | 0.11.x   | iptables-multiport |
| 24.04 LTS | 1.0.x    | nftables           |
| 26.04 LTS | 1.1.x    | nftables           |

Theo LinuxCapable , các phiên bản mới dùng `nftables` thay vì `iptables`.Fail2ban giúp giảm đáng kể các cuộc tấn công brute-force vào SSH và các dịch vụ khác! 🛡️


# install-from-file-deb

Để cài đặt phần mềm từ một tệp .deb trên Ubuntu, bạn có thể sử dụng một trong các phương pháp sau:

### 1. Cài đặt bằng dpkg

Lệnh `dpkg` là công cụ cài đặt gói cơ bản của Debian và các bản phân phối Linux dựa trên Debian, bao gồm Ubuntu.

Các bước:

1. Mở terminal.
2. Di chuyển đến thư mục chứa tệp .deb hoặc chỉ định đường dẫn đầy đủ đến tệp .deb.
3. Chạy lệnh sau để cài đặt:

   ```bash
   sudo dpkg -i tên-gói.deb
   ```

   Ví dụ, nếu tệp .deb của bạn là myapp.deb:

   ```bash
   sudo dpkg -i myapp.deb
   ```

   Nếu có lỗi thiếu phụ thuộc (dependency), bạn có thể chạy:

   ```bash
   sudo apt-get install -f
   ```

   Lệnh này sẽ tự động cài đặt các gói phụ thuộc thiếu.

### 2. Cài đặt bằng apt (Lựa chọn tốt hơn)

`apt` là công cụ quản lý gói dễ sử dụng hơn và có thể xử lý cả các phụ thuộc gói tự động, giúp bạn tránh các vấn đề với các gói phụ thuộc.

Các bước:

1. Di chuyển đến thư mục chứa tệp .deb hoặc chỉ định đường dẫn đầy đủ.
2. Cài đặt gói bằng `apt`:

   ```bash
   sudo apt install ./tên-gói.deb
   ```

Lưu ý:

* `./`: Dấu chấm và dấu gạch chéo trước tên tệp cho biết rằng tệp .deb nằm trong thư mục hiện tại. Nếu tệp .deb nằm ở nơi khác, bạn có thể cung cấp đường dẫn đầy đủ. Ví dụ:

  ```bash
      sudo apt install ./myapp.deb
  ```

Lợi ích của apt:

* `apt` sẽ tự động xử lý và cài đặt các phụ thuộc còn thiếu, điều này giúp giảm thiểu lỗi so với `dpkg`.


# install-snapshot-restore

Dưới đây là quy trình từng bước đầy đủ để tạo và khôi phục Rsync System Snapshot cho Ubuntu Server (máy vật lý, ext4). Quy trình này phù hợp cho backup định kỳ và phục hồi toàn hệ thống khi gặp sự cố.

## PHẦN 1 — TẠO SYSTEM SNAPSHOT

Tạo bản sao toàn bộ hệ thống đang chạy sang một ổ đĩa khác (khuyến nghị dùng ổ riêng, ví dụ `/dev/sdb1`).

### Bước 1️⃣ Chuẩn bị ổ backup

1. Kiểm tra ổ đĩa:

   ```bash
   lsblk
   ```

   Giả sử ổ backup là `/dev/sdb1`.
2. Tạo filesystem nếu chưa có:

   ```bash
   sudo mkfs.ext4 /dev/sdb1
   ```
3. Tạo thư mục mount:

   ```bash
   sudo mkdir -p /backup
   ```
4. Mount ổ backup:

   ```bash
   sudo mount /dev/sdb1 /backup
   ```
5. Kiểm tra:

   ```bash
   df -h
   ```

### Bước 2️⃣ Thực hiện snapshot bằng rsync

Chạy lệnh:

```bash
sudo rsync -aAXHv --delete \
--exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found","/backup/*"} \
/ /backup
```

Giải thích tham số quan trọng:

* `-a` → giữ nguyên quyền, owner, symlink
* `-A` → giữ ACL
* `-X` → giữ extended attributes
* `--delete` → xóa file trên backup nếu đã xóa ở hệ thống chính

⏳ Thời gian phụ thuộc dung lượng dữ liệu.

### Bước 3️⃣ Cài lại GRUB vào bản backup (quan trọng)

1. Mount system cần thiết:

   ```bash
   sudo mount --bind /dev /backup/dev
   sudo mount --bind /proc /backup/proc
   sudo mount --bind /sys /backup/sys
   ```
2. Chroot vào hệ thống backup:

   ```bash
   sudo chroot /backup
   ```
3. Cài lại bootloader:

   ```bash
   grub-install /dev/sda
   update-grub
   ```
4. Thoát:

   ```bash
   exit
   ```
5. Unmount:

   ```bash
   sudo umount /backup/dev
   sudo umount /backup/proc
   sudo umount /backup/sys
   ```

***

## PHẦN 2 — RESTORE KHI HỆ THỐNG HỎNG

### Trường hợp: Server không boot được

#### Bước 1️⃣ Boot bằng Ubuntu Live USB

Sử dụng ISO cùng version (ví dụ 22.04).

#### Bước 2️⃣ Mount phân vùng chính

```bash
sudo mount /dev/sda1 /mnt
```

Mount ổ backup:

```bash
sudo mount /dev/sdb1 /backup
```

#### Bước 3️⃣ Restore hệ thống

```bash
sudo rsync -aAXHv --delete /backup/ /mnt/
```

#### Bước 4️⃣ Reinstall GRUB

```bash
sudo mount --bind /dev /mnt/dev
sudo mount --bind /proc /mnt/proc
sudo mount --bind /sys /mnt/sys
sudo chroot /mnt
grub-install /dev/sda
update-grub
exit
```

Unmount:

```bash
sudo umount /mnt/dev
sudo umount /mnt/proc
sudo umount /mnt/sys
```

Reboot:

```bash
sudo reboot
```

***

## Kiểm tra snapshot thành công

Trên server đang chạy:

```bash
sudo rsync -navc / /backup
```

Nếu không hiển thị file khác biệt lớn → snapshot ổn định.

***

## Tự động hóa snapshot định kỳ

Tạo script:

```bash
sudo nano /usr/local/bin/system-backup.sh
```

Nội dung:

```bash
#!/bin/bash
mount /dev/sdb1 /backup
rsync -aAXHv --delete \
--exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found","/backup/*"} \
/ /backup
umount /backup
```

Phân quyền:

```bash
sudo chmod +x /usr/local/bin/system-backup.sh
```

Thêm cron job:

```bash
sudo crontab -e
```

Ví dụ chạy mỗi 2h sáng:

```bash
0 2 * * * /usr/local/bin/system-backup.sh
```


# install-wine

### 🍷 Cách 5: Wine (Chạy Zalo PC Windows)

Nếu muốn dùng bản chính thức từ zalo.me :bash Copy

```bash
# Cài Wine
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install wine64 wine32 -y

# Tải Zalo PC từ zalo.me
wget https://res-download-pc-te-vnno-zn-6.zadn.vn/hybrid/ZaloSetup-25.1.2.exe

# Cài đặt
wine ZaloSetup-*.exe
```

⚠️ **Lưu ý**: Wine có thể không hoạt động ổn định với Zalo PC mới nhất.


# setup-youtube-stream

Dưới đây là hướng dẫn chi tiết cài đặt ứng dụng live stream màn hình lên YouTube trên Ubuntu với nhiều lựa chọn từ đơn giản đến chuyên nghiệp:

### 🎬 Lựa chọn 1: OBS Studio (Khuyến nghị - Chuyên nghiệp nhất)

#### Cài đặt OBS Studio

bash Copy

```bash
# Cách 1: Từ PPA chính thức (khuyến nghị cho Ubuntu)
sudo add-apt-repository ppa:obsproject/obs-studio
sudo apt update
sudo apt install obs-studio -y

# Cách 2: Từ Snap
sudo snap install obs-studio

# Cách 3: Từ Flatpak
flatpak install flathub com.obsproject.Studio
```

Theo OBS Project , OBS Studio là phần mềm stream/record miễn phí, mã nguồn mở, hỗ trợ đa nền tảng.

#### Cấu hình stream YouTube

1. **Mở OBS Studio**
2. **Settings** → **Stream**:
   * **Service**: YouTube / YouTube - RTMPS
   * **Server**: Primary YouTube ingest server
   * **Stream Key**: Lấy từ [YouTube Studio](https://studio.youtube.com) → Go Live → Stream
3. **Settings** → **Output**:
   * **Video Bitrate**: 2500-6000 Kbps (tùy mạng)
   * **Encoder**: x264 hoặc NVENC (nếu có GPU NVIDIA)
4. **Settings** → **Video**:
   * **Base Resolution**: 1920x1080
   * **Output Resolution**: 1280x720 (hoặc 1920x1080)
   * **FPS**: 30 hoặc 60
5. **Sources** → **+** → **Screen Capture** để chọn màn hình stream

***

### 🖥️ Lựa chọn 2: FFmpeg (Dòng lệnh - Nhẹ, tự động hóa)

#### Cài đặt FFmpeg

bash Copy

```bash
sudo apt update
sudo apt install ffmpeg -y
```

#### Stream màn hình + audio lên YouTube

bash Copy

```bash
# Lấy stream key từ YouTube Studio trước
STREAM_KEY="your-stream-key-here"

# Stream màn hình full HD với audio
ffmpeg -f x11grab -framerate 30 -video_size 1920x1080 -i :0.0 \
-f pulse -i default \
-c:v libx264 -preset fast -pix_fmt yuv420p -maxrate 3000k -bufsize 6000k -g 60 \
-c:a aac -b:a 128k -ar 44100 \
-f flv rtmp://a.rtmp.youtube.com/live2/$STREAM_KEY
```

**Giải thích tham số** :

* `-f x11grab`: Bắt màn hình X11
* `-framerate 30`: 30 FPS
* `-video_size 1920x1080`: Độ phân giải
* `-i :0.0`: Màn hình đầu tiên
* `-f pulse -i default`: Ghi âm từ PulseAudio (desktop audio)
* `-preset fast`: Tốc độ encode (ultrafast → veryslow)
* `-maxrate 3000k`: Bitrate tối đa
* `-g 60`: Keyframe mỗi 2 giây (60/30fps)

#### Script tự động stream

Tạo file `youtube_stream.sh`:bash Copy

```bash
#!/bin/bash

# Cấu hình
STREAM_KEY="xxxx-xxxx-xxxx-xxxx-xxxx"  # Thay bằng stream key của bạn
RESOLUTION="1920x1080"
FPS="30"
BITRATE="3000k"

# Kiểm tra stream key
if [ "$STREAM_KEY" = "xxxx-xxxx-xxxx-xxxx-xxxx" ]; then
    echo "❌ Vui lòng cập nhật STREAM_KEY trong script!"
    exit 1
fi

echo "🎬 Bắt đầu stream lên YouTube..."
echo "📺 Resolution: $RESOLUTION | 🎞️ FPS: $FPS | 📊 Bitrate: $BITRATE"

ffmpeg -f x11grab -framerate $FPS -video_size $RESOLUTION -i :0.0 \
-f pulse -i default \
-c:v libx264 -preset fast -pix_fmt yuv420p \
-maxrate $BITRATE -bufsize $((${BITRATE%k}*2))k \
-g $((FPS*2)) \
-c:a aac -b:a 128k -ar 44100 \
-f flv rtmp://a.rtmp.youtube.com/live2/$STREAM_KEY

echo "🛑 Stream đã dừng"
```

Chạy script:bash Copy

```bash
chmod +x youtube_stream.sh
./youtube_stream.sh
```

***

### 📹 Lựa chọn 3: SimpleScreenRecorder + FFmpeg

SimpleScreenRecorder không stream trực tiếp được , nhưng có thể kết hợp với FFmpeg:bash Copy

```bash
# Cài đặt SimpleScreenRecorder
sudo apt install simplescreenrecorder -y

# Ghi màn hình ra file, sau đó stream file đó
ffmpeg -re -i recorded_video.mp4 -c:v libx264 -preset fast -c:a aac -f flv rtmp://a.rtmp.youtube.com/live2/KEY
```

***

### 🎯 Lựa chọn 4: YouTube Live Streamer (Snap - Đơn giản cho IP Cam)

Nếu bạn muốn stream từ camera IP hoặc nguồn video có sẵn :bash Copy

```bash
sudo snap install youtube-live-streamer --edge

# Cấu hình
sudoedit /var/snap/youtube-live-streamer/common/live-streamer.conf

# Thêm stream key vào file cấu hình
# youtube-stream-key: "your-stream-key-here"

# Khởi động lại
sudo snap restart youtube-live-streamer
```

***

### 🔧 Cấu hình nâng cao

#### 1. Chọn nguồn audio chính xác (PulseAudio)

bash Copy

```bash
# Liệt kê các nguồn audio
pactl list sources | grep Name

# Stream với nguồn cụ thể (ví dụ: monitor của desktop)
ffmpeg -f x11grab ... -f pulse -i alsa_output.pci-0000_00_1f.3.analog-stereo.monitor ...
```

#### 2. Stream có overlay webcam

bash Copy

```bash
ffmpeg -f x11grab -video_size 1920x1080 -i :0.0 \
-f v4l2 -video_size 320x240 -i /dev/video0 \
-filter_complex "[0:v][1:v]overlay=main_w-overlay_w-10:main_h-overlay_h-10[out]" \
-map "[out]" -map 1:a \
-c:v libx264 -preset fast -f flv rtmp://a.rtmp.youtube.com/live2/KEY
```

#### 3. Stream 24/7 với video lặp (VPS/Server)

Theo Zomro , tạo script Python:Python Copy

```python
# stream_loop.py
import os
import time

video_dir = "/path/to/videos/"
stream_key = "your-stream-key"
rtmp_url = f"rtmp://a.rtmp.youtube.com/live2/{stream_key}"

while True:
    for file in os.listdir(video_dir):
        if file.endswith((".mp4", ".mkv", ".avi")):
            cmd = f'ffmpeg -re -i "{video_dir}{file}" -c:v libx264 -preset ultrafast -crf 24 -g 3 -f flv {rtmp_url}'
            os.system(cmd)
            time.sleep(1)
```

***

### 📋 Script tổng hợp: Cài đặt tự động

Tạo file `setup_youtube_stream.sh`:bash Copy

```bash
#!/bin/bash

echo "╔════════════════════════════════════════════════════════════╗"
echo "║    CÀI ĐẶT MÔI TRƯỜNG STREAM YOUTUBE CHO UBUNTU           ║"
echo "╚════════════════════════════════════════════════════════════╝"

# Cập nhật hệ thống
sudo apt update && sudo apt upgrade -y

# Cài đặt FFmpeg
echo "📦 Cài đặt FFmpeg..."
sudo apt install ffmpeg -y

# Cài đặt OBS Studio (tùy chọn)
read -p "Cài đặt OBS Studio? (y/N): " install_obs
if [[ "$install_obs" =~ ^[Yy]$ ]]; then
    sudo add-apt-repository ppa:obsproject/obs-studio -y
    sudo apt update
    sudo apt install obs-studio -y
    echo "✅ OBS Studio đã cài đặt"
fi

# Cài đặt SimpleScreenRecorder (tùy chọn)
read -p "Cài đặt SimpleScreenRecorder? (y/N): " install_ssr
if [[ "$install_ssr" =~ ^[Yy]$ ]]; then
    sudo apt install simplescreenrecorder -y
    echo "✅ SimpleScreenRecorder đã cài đặt"
fi

# Tạo thư mục scripts
mkdir -p ~/youtube-stream
cd ~/youtube-stream

# Tạo script stream
cat > stream_desktop.sh << 'EOF'
#!/bin/bash
read -p "Nhập YouTube Stream Key: " KEY
read -p "Resolution (1920x1080/1280x720): " RES
RES=${RES:-1920x1080}

echo "🎬 Đang stream với resolution: $RES"
echo "Nhấn Ctrl+C để dừng"

ffmpeg -f x11grab -framerate 30 -video_size $RES -i :0.0 \
-f pulse -i default \
-c:v libx264 -preset fast -pix_fmt yuv420p -maxrate 3000k -bufsize 6000k -g 60 \
-c:a aac -b:a 128k -ar 44100 \
-f flv rtmp://a.rtmp.youtube.com/live2/$KEY
EOF

chmod +x stream_desktop.sh

echo ""
echo "✅ HOÀN TẤT!"
echo "📁 Script stream được lưu tại: ~/youtube-stream/stream_desktop.sh"
echo ""
echo "🚀 Cách sử dụng:"
echo "   cd ~/youtube-stream"
echo "   ./stream_desktop.sh"
echo ""
echo "📺 Hoặc mở OBS Studio để stream với giao diện đồ họa"
echo ""
echo "🔑 Lấy Stream Key tại: https://studio.youtube.com → Go Live → Stream"
```

Chạy script cài đặt:bash Copy

```bash
chmod +x setup_youtube_stream.sh
./setup_youtube_stream.sh
```

***

### 🎮 Lựa chọn theo nhu cầu

Table

| Nhu cầu                   | Công cụ khuyến nghị           | Lý do                           |
| ------------------------- | ----------------------------- | ------------------------------- |
| **Stream chuyên nghiệp**  | OBS Studio                    | Nhiều tính năng, scene, overlay |
| **Nhẹ, tự động hóa**      | FFmpeg                        | Dòng lệnh, script hóa được      |
| **Ghi + Stream sau**      | SimpleScreenRecorder + FFmpeg | Ghi chất lượng cao              |
| **Stream 24/7 từ server** | FFmpeg + Script Python        | Không cần GPU, chạy VPS         |
| **IP Camera → YouTube**   | YouTube Live Streamer (Snap)  | Đơn giản, cấu hình nhanh        |

***

### ⚠️ Lưu ý quan trọng

1. **Stream Key**: Giữ bí mật, không chia sẻ công khai
2. **Bitrate**: Điều chỉnh theo tốc độ upload (test tại speedtest.net)
3. **CPU Usage**: Nếu CPU cao, giảm `preset` (ultrafast) hoặc dùng NVENC/VAAPI
4. **Audio**: Kiểm tra nguồn audio đúng bằng `pavucontrol`
5. **YouTube**: Stream key thay đổi mỗi lần tạo live mới (trừ khi dùng Scheduled Stream)

Chúc bạn stream thành công! 🎉


# manage-users

### Thêm người dùng vào nhóm

Xem danh sách nhóm:

```bash
getent group

# --> root@minipc-quyit:/# getent group
# root:x:0:
# nhquydev:x:1001:minipc
```

Xem nhóm của người dùng hiện tại:

```bash
root@minipc-quyit:/# groups
# root ollama docker

minipc@minipc-quyit:~$ groups
# minipc adm cdrom sudo dip plugdev lxd docker
```

Thông tin chi tiết về người dùng:

```bash
root@minipc-quyit:~# id
# uid=0(root) gid=0(root) groups=0(root),985(ollama),986(docker)
```

Để thêm người dùng minipc vào nhóm nhquydev, bạn sử dụng lệnh sau:

```bash
# sudo usermod -aG group_name user_name
sudo usermod -aG nhquydev minipc

# sudo gpasswd -d user_name group_name
# muốn gỡ user minipc khỏi nhóm root
sudo gpasswd -d minipc root
```

Kiểm tra xem người dùng đã được thêm vào nhóm chưa

```bash
groups minipc

# --> minipc : minipc nhquydev
```


# mount-disk-partition

### Xem danh sách disk

Để xem các ổ đĩa và phân vùng có trong máy Ubuntu Server, bạn có thể sử dụng một số lệnh sau:

1. Lệnh lsblk: Đây là lệnh đơn giản để liệt kê các ổ đĩa và phân vùng. Bạn có thể chạy lệnh này trong terminal:

   ```bash
   lsblk
   ```

   Kết quả sẽ cho bạn thông tin về các ổ đĩa (disk), phân vùng (partition), và điểm gắn kết (mount point).
2. Lệnh fdisk -l: Lệnh này giúp hiển thị thông tin chi tiết về các ổ đĩa và phân vùng trên hệ thống. Cần quyền root, vì vậy bạn phải sử dụng sudo:

   ```bash
   sudo fdisk -l
   ```
3. Lệnh df -h: Lệnh này hiển thị thông tin về dung lượng của các phân vùng đã được gắn kết. Tùy chọn -h giúp hiển thị kích thước theo định dạng dễ đọc (GB, MB, KB):

   ```bash
   df -h
   ```
4. Lệnh parted -l: Lệnh này cung cấp thông tin chi tiết về các phân vùng và ổ đĩa trong hệ thống. Cũng cần quyền root:

   ```bash
   sudo parted -l
   ```

### Mount Partition

#### 1. Kiểm tra các phân vùng trên ổ đĩa

Trước khi mount, bạn cần biết chính xác phân vùng mà bạn muốn mount. Dựa trên kết quả từ sudo `parted -l`, ổ đĩa /dev/nvme0n1 có 2 phân vùng:

* **/dev/nvme0n1p1** (524GB, NTFS)
* **/dev/nvme0n1p2** (476GB, NTFS)

Giả sử bạn muốn mount phân vùng **/dev/nvme0n1p1**.

#### 2. Tạo thư mục để mount

Trước khi mount phân vùng, bạn cần tạo một thư mục mà phân vùng sẽ được gắn vào (ví dụ: /mnt/d01).

```bash
sudo mkdir /mnt/d01
```

#### 3. Mount phân vùng vào thư mục

Giờ bạn có thể mount phân vùng vào thư mục vừa tạo. Ví dụ:

```bash
sudo mount /dev/nvme0n1p1 /mnt/d01
```

#### 4. Kiểm tra xem phân vùng đã được mount chưa

Bạn có thể kiểm tra xem phân vùng đã được mount thành công chưa bằng cách sử dụng lệnh `df -h` hoặc `lsblk`:

```bash
df -h
```

Hoặc:

```bash
lsblk
```

#### 5. Mount tự động khi khởi động lại (optional)

Nếu bạn muốn phân vùng được mount tự động mỗi khi hệ thống khởi động lại, bạn cần thêm thông tin vào file `/etc/fstab`.

1. Mở file `/etc/fstab` bằng trình soạn thảo văn bản:

   ```bash
   sudo nano /etc/fstab
   ```
2. Thêm dòng sau vào cuối file (đảm bảo thay `/dev/nvme0n1p1` và `/mnt/d01` nếu bạn sử dụng phân vùng hoặc thư mục khác):

   ```bash
   /dev/nvme0n1p1  /mnt/d01  ntfs  defaults  0  0
   ```
3. Lưu và thoát (nhấn Ctrl + X, sau đó nhấn Y để lưu và Enter để thoát).

#### 6. Kiểm tra lại fstab

Để đảm bảo rằng bạn không có lỗi trong file `/etc/fstab`, bạn có thể chạy lệnh kiểm tra:

```bash
sudo mount -a
```

Nếu không có thông báo lỗi, phân vùng đã được cấu hình mount tự động khi khởi động lại.

***

### Read and mount file `.vhdx` trên Ubuntu server

#### 1. Cài đặt các công cụ cần thiết:

Trước tiên, bạn cần cài đặt qemu-utils để hỗ trợ thao tác với file .vhdx.

```bash
sudo apt update
sudo apt install qemu-utils -y
```

#### 2. Mount file `.vhdx`:

Sau khi cài đặt xong, bạn có thể sử dụng `qemu-nbd` (Network Block Device) để mount file `.vhdx`.

* Bước 1: Kích hoạt module `nbd`.

  ```bash
  sudo modprobe nbd max_part=8
  ```
* Bước 2: Sử dụng qemu-nbd để kết nối file .vhdx với một thiết bị block.

  ```bash
  # Kết nối tệp VHDX "quyit.vhdx" vào thiết bị mạng block /dev/nbd0
  sudo qemu-nbd --connect=/dev/nbd0 /mnt/d02/devdrives/quyit.vhdx

  # Kết nối tệp VHDX "ltk1005.vhdx" vào thiết bị mạng block /dev/nbd1
  sudo qemu-nbd --connect=/dev/nbd1 /mnt/d02/devdrives/ltk1005.vhdx
  ```
* Bước 3: Kiểm tra các phân vùng trong .vhdx (nếu có):

  ```bash
  # Hiển thị thông tin phân vùng của thiết bị mạng block /dev/nbd0
  sudo fdisk -l /dev/nbd0

  # Hiển thị thông tin phân vùng của thiết bị mạng block /dev/nbd1
  sudo fdisk -l /dev/nbd1
  ```
* Bước 4: Mount phân vùng (ví dụ nếu phân vùng là /dev/nbd0p1):

  ```bash
  # Mount phân vùng thứ hai (p2) của thiết bị /dev/nbd0 vào thư mục /mnt/quyit
  sudo mount /dev/nbd0p2 /mnt/quyit

  # Mount phân vùng thứ hai (p2) của thiết bị /dev/nbd1 vào thư mục /mnt/ltk1005
  sudo mount /dev/nbd1p2 /mnt/ltk1005
  ```

Giờ bạn đã có thể truy cập nội dung của file `.vhdx` trong thư mục `/mnt`.

#### 3. Gỡ mount và ngắt kết nối:

Sau khi hoàn tất việc sử dụng file `.vhdx`, bạn có thể gỡ mount và ngắt kết nối như sau:

* Bước 1: Unmount phân vùng:

  ```bash
  # Hiển thị tất cả các phân vùng đang được mount, lọc kết quả để chỉ hiển thị những phân vùng liên quan đến /dev/nbd
  mount | grep /dev/nbd

  # Ngắt kết nối phân vùng đang được mount tại thư mục /mnt/quyit
  sudo umount /mnt/quyit

  # Ngắt kết nối phân vùng đang được mount tại thư mục /mnt/ltk1005
  sudo umount /mnt/ltk1005
  ```
* Bước 2: Ngắt kết nối nbd:

  ```bash
  # Ngắt kết nối tệp ảnh đĩa đã kết nối vào thiết bị mạng block /dev/nbd0
  sudo qemu-nbd --disconnect /dev/nbd0

  # Ngắt kết nối tệp ảnh đĩa đã kết nối vào thiết bị mạng block /dev/nbd1
  sudo qemu-nbd --disconnect /dev/nbd1
  ```

***


# shared-folder

Để cấu hình chia sẻ thư mục trên Ubuntu Server, bạn có thể sử dụng Samba. Dưới đây là các bước cơ bản để chia sẻ thư mục trong Ubuntu Server qua Samba.

### 1. Cài đặt Samba

Đầu tiên, bạn cần cài đặt Samba trên máy chủ Ubuntu của mình:

```bash
sudo apt update
sudo apt install samba
```

### 2. Tạo thư mục để chia sẻ

Tiếp theo, tạo một thư mục mà bạn muốn chia sẻ. Ví dụ, bạn có thể tạo thư mục /srv/samba/shared:

```bash
sudo mkdir -p /srv/samba/shared
```

Sau đó, bạn cần cấp quyền cho thư mục đó để người dùng có thể truy cập. Ví dụ, nếu bạn muốn mọi người có quyền đọc và ghi, bạn có thể thực hiện lệnh sau:

```bash
sudo chmod 777 /srv/samba/shared
```

### 3. Cấu hình Samba

Tiếp theo, bạn cần cấu hình Samba để chia sẻ thư mục. Mở file cấu hình của Samba:

```bash
sudo nano /etc/samba/smb.conf
```

Cuộn xuống phần dưới cùng của file và thêm cấu hình cho thư mục chia sẻ của bạn:

```ini
[shared]
   path = /srv/samba/shared
   browsable = yes
   writable = yes
   guest ok = yes
   read only = no
```

* \[shared]: Đây là tên chia sẻ (người dùng sẽ thấy khi kết nối).
* path: Đường dẫn đến thư mục bạn muốn chia sẻ.
* browsable: Cho phép thư mục hiển thị trong mạng.
* writable: Cho phép ghi vào thư mục.
* guest ok: Cho phép truy cập mà không cần mật khẩu (nếu bạn muốn bảo mật hơn, có thể tắt guest ok).
* read only: Đặt giá trị no nếu bạn muốn thư mục có thể ghi.

### 4. Tạo người dùng Samba (nếu cần)

Nếu bạn không muốn chia sẻ thư mục cho tất cả mọi người và muốn yêu cầu mật khẩu, bạn có thể tạo người dùng Samba:

`sudo smbpasswd -a username`

Thay username bằng tên người dùng mà bạn muốn tạo, sau đó nhập mật khẩu.

### 5. Khởi động lại Samba

Sau khi cấu hình xong, bạn cần khởi động lại dịch vụ Samba để áp dụng thay đổi:

`sudo systemctl restart smbd`

### 6. Kiểm tra kết nối

Bây giờ, bạn có thể kiểm tra xem thư mục có được chia sẻ đúng không bằng cách kết nối từ máy Windows hoặc Linux khác.

Trên Windows: Mở File Explorer và nhập địa chỉ IP của máy chủ Ubuntu vào thanh địa chỉ, ví dụ: `\\192.168.1.100\shared`.

Trên Linux: Sử dụng lệnh smbclient:

`smbclient //192.168.1.100/shared -U username`

Nếu mọi thứ được cấu hình đúng, bạn sẽ thấy thư mục chia sẻ.

Lưu ý:

Đảm bảo rằng tường lửa (firewall) của bạn cho phép kết nối với Samba (cổng 445 và 139).

Bạn có thể mở cổng Samba bằng lệnh sau:

`sudo ufw allow samba`

Đó là tất cả các bước cơ bản để chia sẻ thư mục trên Ubuntu Server với Samba!


# update-systemd-resolved

### Cấu hình `systemd-resolved` để không ghi đè `resolv.conf`:

Nếu bạn không muốn tắt `systemd-resolved` hoàn toàn nhưng vẫn muốn giữ DNS tĩnh, bạn có thể cấu hình lại `systemd-resolved` để không ghi đè vào file `resolv.conf`.

#### Bước 1: Sửa cấu hình systemd-resolved để sử dụng DNS tĩnh:

Mở file cấu hình của `systemd-resolved`:

```bash
sudo nano /etc/systemd/resolved.conf
```

#### Bước 2: Cấu hình DNS tĩnh trong file này, ví dụ:

```ini
[Resolve]
DNS=1.1.1.1 8.8.8.8
FallbackDNS=1.0.0.1 8.8.4.4
```

#### Bước 3: Tạo lại symlink `/etc/resolv.conf`:

```bash
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
```

#### Bước 4: Khởi động lại `systemd-resolved`:

```bash
sudo systemctl restart systemd-resolved
```

#### Bước 5: Kiểm tra lại DNS:

```bash
resolvectl status
```

Với cách này, `systemd-resolved` sẽ sử dụng DNS mà bạn đã chỉ định trong cấu hình và sẽ không ghi đè nữa.

***


# wireguard-activate

## WireGuard

### WireGuard Activate

1. Tạo file demon service activate

```bash
nano /etc/systemd/system/wg-http-activate.service

# enable service
systemctl daemon-reload
systemctl enable wg-http-activate.service
```

Nội dung file `wg-http-activate.service`

```ini
[Unit]
Description=Activate WireGuard HTTP Trigger
After=network-online.target wg-quick@wg0.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/u01/crontab/00-start-wg.sh

[Install]
WantedBy=multi-user.target
```

2. Tạo script start wireguard

```bash
nano /u01/crontab/00-start-wg.sh
```

Nội dung file `00-start-wg.sh`

```bash
#!/bin/bash

WG_INTERFACE="wg0"
CLIENT_NAME="${CLIENT_NAME:-client1}"
PING_IP="10.8.0.1"
SERVER_URL="${SERVER_URL:-http://server_address/healthcheck}"
URL="http://10.8.0.1"

# neu wg chua chay thi bat
if ! /usr/bin/wg show $WG_INTERFACE >/dev/null 2>&1; then
  /usr/bin/wg-quick up $WG_INTERFACE
fi

# doi interface on dinh
sleep 5

# kiem tra neu chua ping duoc thi goi http de kich hoat
if ! ping -c 1 $PING_IP >/dev/null 2>&1; then
  /usr/bin/curl -m 5 $URL >/dev/null 2>&1
fi

exit 0


```


# docker-move-data-root

## Hướng dẫn chuyển Docker `data-root` sang `/mnt/e02/uenv/var/lib/docker`

### 1. Dừng Docker

Trước khi di chuyển dữ liệu cần dừng Docker để tránh lỗi filesystem.

```
sudo systemctl stop docker
```

Kiểm tra đã dừng:

```
sudo systemctl status docker
```

***

### 2. Tạo thư mục data-root mới

```
sudo mkdir -p /mnt/e02/uenv/var/lib/docker
```

***

### 3. Copy toàn bộ dữ liệu Docker hiện tại

Sử dụng `rsync` để giữ nguyên permission, link và metadata.

```
sudo rsync -aP /var/lib/docker/ /mnt/e02/uenv/var/lib/docker/
```

Giải thích:

* `-a` giữ nguyên quyền
* `-P` hiển thị progress
* dấu `/` cuối thư mục rất quan trọng

***

### 4. Cấu hình Docker dùng data-root mới

Mở file cấu hình:

```
sudo nano /etc/docker/daemon.json
```

Thêm hoặc chỉnh thành:

```
{
  "data-root": "/mnt/e02/uenv/var/lib/docker"
}
```

Nếu file đã có cấu hình khác, chỉ cần thêm `"data-root"` vào trong `{}`.

***

### 5. Khởi động lại Docker

```
sudo systemctl daemon-reexec
sudo systemctl start docker
```

***

### 6. Kiểm tra Docker đã dùng data-root mới

```
docker info | grep "Docker Root Dir"
```

Kết quả mong muốn:

```
Docker Root Dir: /mnt/e02/uenv/var/lib/docker
```

***

### 7. Kiểm tra container

```
docker ps -a
```

Nếu container và image vẫn còn đầy đủ thì việc chuyển thành công.

***

### 8. (Tùy chọn) Xóa dữ liệu cũ

Sau khi chạy ổn định, bạn có thể xóa thư mục cũ để giải phóng dung lượng.

```
sudo rm -rf /var/lib/docker
```

***

### Kết quả sau khi chuyển

Docker sẽ ghi dữ liệu vào:

```
/mnt/e02/uenv/var/lib/docker/
```

Các thư mục bên trong gồm:

```
overlay2
containers
image
volumes
network
buildkit
```

Các mount overlay kiểu bạn thấy trước đó:

```
/var/lib/docker/rootfs/overlayfs/...
```

sẽ chuyển thành:

```
/mnt/e02/uenv/var/lib/docker/rootfs/overlayfs/...
```

***

💡 **Khuyến nghị thêm cho server chạy Docker nhiều container**

Ổ `/mnt/e02` của bạn còn \~297GB nên rất phù hợp. Tuy nhiên nên kiểm tra filesystem:

```
df -Th /mnt/e02
```

Docker **tốt nhất chạy trên ext4 hoặc xfs**.

### Scripts `01_docker_move_data_root.sh`

{% code title="01\_docker\_move\_data\_root.sh" overflow="wrap" lineNumbers="true" %}

```bash
#!/bin/bash

# Thư mục Docker cũ và mới
OLD_DOCKER="/var/lib/docker"
NEW_DOCKER="/mnt/e02/uenv/var/lib/docker"

echo "⚠️ Dừng tất cả container Docker..."
sudo docker stop $(sudo docker ps -q) 2>/dev/null

echo "📁 Tạo thư mục Docker mới nếu chưa có..."
sudo mkdir -p "$NEW_DOCKER"

echo "🔄 Sao chép dữ liệu Docker cũ sang thư mục mới..."
sudo rsync -aP "$OLD_DOCKER/" "$NEW_DOCKER/"

echo "📝 Cập nhật /etc/docker/daemon.json..."
# Nếu file daemon.json chưa có, tạo mới
if [ ! -f /etc/docker/daemon.json ]; then
    sudo bash -c "echo '{\"data-root\": \"$NEW_DOCKER\"}' > /etc/docker/daemon.json"
else
    # Nếu đã có nội dung, sửa hoặc thêm data-root
    sudo jq --arg newroot "$NEW_DOCKER" '. + { "data-root": $newroot }' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json > /dev/null
fi

echo "🔁 Khởi động lại Docker..."
sudo systemctl daemon-reload
sudo systemctl restart docker

echo "✅ Hoàn tất!"
echo "Docker root mới là:"
docker info | grep "Docker Root Dir"

```

{% endcode %}


# TÀI LIỆU SPEC: ỨNG DỤNG VIẾT TRUYỆN AI

### **StoryForge AI - Technical Specification**

***

### 1. TỔNG QUAN HỆ THỐNG

#### 1.1 Mục tiêu

Xây dựng ứng dụng web cho phép người dùng tải lên file outline truyện (PDF/Word/TXT), AI phân tích và viết thành truyện hoàn chỉnh theo các thể loại tùy chọn.

#### 1.2 Người dùng mục tiêu

* Nhà văn mới cần hỗ trợ viết truyện
* Content creator cần sản xuất nội dung nhanh
* Người có ý tưởng nhưng chưa biết cách triển khai

***

### 2. CHỨC NĂNG CHÍNH

#### 2.1 Module Upload & Phân tích Outline

Table Copy

| Chức năng              | Mô tả                                                   | Input          | Output                |
| ---------------------- | ------------------------------------------------------- | -------------- | --------------------- |
| **File Upload**        | Hỗ trợ PDF, DOCX, TXT                                   | File outline   | Text trích xuất       |
| **Structure Parser**   | Phân tích cấu trúc outline                              | Raw text       | JSON cấu trúc         |
| **Content Classifier** | Phát hiện thể loại, độ tuổi, cảnh báo nội dung nhạy cảm | Parsed content | Tags + Content Rating |

**Cấu trúc JSON sau khi parse:**

JSON Copy

```json
{
  "metadata": {
    "title": "Y tá nhỏ của tôi",
    "tagline": "Em 20, lần đầu biết yêu...",
    "genre_detected": ["romance", "drama", "medical"],
    "content_rating": "R-18",
    "warning_flags": ["explicit_sexual_content", "age_gap_relationship"]
  },
  "characters": [
    {
      "name": "Trinh",
      "age": 20,
      "role": "protagonist",
      "occupation": "y tá",
      "traits": ["ngây thơ", "xinh đẹp", "mới ra trường"],
      "physical_description": "dáng thanh mảnh, da trắng hồng..."
    }
  ],
  "chapters": [
    {
      "chapter_number": 1,
      "title": "Cô gái nhỏ vào khoa Ngoại",
      "setting": "Bệnh viện tỉnh, khoa Ngoại",
      "key_scenes": [
        {
          "scene_id": "1.1",
          "location": "phòng thay đồ",
          "characters": ["Trinh"],
          "action": "Trinh chuẩn bị đi làm ngày đầu tiên",
          "sensory_details": ["đồng phục xanh nhạt", "áo blouse trắng"],
          "emotional_tone": "hồi hộp, tự tin"
        }
      ]
    }
  ],
  "sensory_elements": {
    "visual": ["màu xanh nhạt", "ánh mắt sâu thẳm"],
    "auditory": ["tiếng mưa rơi", "tiếng giường kêu"],
    "tactile": ["da mịn như lụa", "bàn tay khô ráp"],
    "olfactory": ["mùi bạc hà", "mùi thuốc sát trùng"]
  }
}
```

***

#### 2.2 Module AI Writing Engine

**2.2.1 Content Policy Filter**

plain Copy

```
┌─────────────────────────────────────────┐
│         INPUT: Parsed Outline           │
└─────────────────┬───────────────────────┘
                  ▼
┌─────────────────────────────────────────┐
│    CONTENT SAFETY CLASSIFIER            │
│  • Detect sexual content level          │
│  • Detect violence, sensitive themes  │
│  • Assign content rating                │
└─────────────────┬───────────────────────┘
                  ▼
        ┌─────────┴─────────┐
        ▼                   ▼
   [SAFE CONTENT]      [EXPLICIT CONTENT]
        │                   │
        ▼                   ▼
┌───────────────┐    ┌──────────────────┐
│ Direct Writing│    │ Reframe Options  │
│               │    │ • Romance (fade  │
│               │    │   to black)      │
│               │    │ • Drama (focus   │
│               │    │   on emotion)    │
│               │    │ • Skip scene     │
└───────────────┘    └──────────────────┘
```

**2.2.2 Writing Modes**

Table Copy

| Mode               | Mô tả                             | Phù hợp với            |
| ------------------ | --------------------------------- | ---------------------- |
| **Romance-Pure**   | Tình cảm sâu sắc, không cảnh nóng | Người đọc mọi lứa tuổi |
| **Romance-Steamy** | Gợi cảm nhẹ, ngụ ý                | Người trưởng thành     |
| **Drama**          | Xung đột, phát triển nhân vật     | Thích truyện chiều sâu |
| **Rom-Com**        | Hài hước, nhẹ nhàng               | Giải trí               |
| **Thriller**       | Căng thẳng, bí ẩn                 | Truyện trinh thám      |
| **Slice of Life**  | Đời thường, chậm rãi              | Thư giãn               |

**2.2.3 Scene Rewriting Rules (cho nội dung nhạy cảm)**

**Ví dụ chuyển đổi từ outline gốc:**&#x54;able Copy

| Gốc (Explicit)                       | Romance-Pure                                                                                                        | Drama                                                                                    |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| "Cảnh nóng trong phòng trực đêm mưa" | "Khoảnh khắc gần gũi đầu tiên - họ trao nhau nụ hôn dưới ánh đèn khẩn cấp, tình cảm vượt qua ranh giới bác sĩ-y tá" | "Xung đột nội tâm của An khi rung động trước đồng nghiệp trẻ, sự cô đơn sau ca trực dài" |
| "Tay chạm vào vòng một"              | "Ánh mắt dừng lại ở nơi anh không nên nhìn, cả hai đều biết điều gì đang thay đổi"                                  | "Sự chạm va vô tình khiến An nhận ra mình đã lâu không gần gũi ai"                       |

***

#### 2.3 Module Output & Export

Table Copy

| Format         | Mô tả                  | Tùy chọn              |
| -------------- | ---------------------- | --------------------- |
| **Web Reader** | Đọc trực tiếp trên app | Font, theme, bookmark |
| **EPUB**       | Ebook chuẩn            | Cover, metadata       |
| **PDF**        | In ấn                  | Layout A4/A5          |
| **DOCX**       | Chỉnh sửa tiếp         | Track changes         |
| **Audio**      | Text-to-speech         | Voice selection       |

***

### 3. KIẾN TRÚC KỸ THUẬT

#### 3.1 Tech Stack

plain Copy

```
┌─────────────────────────────────────────┐
│           FRONTEND (Next.js)            │
│  • React 18 + TypeScript                │
│  • TailwindCSS + shadcn/ui              │
│  • React Query (state management)         │
│  • React Hook Form + Zod (validation)   │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│           BACKEND (Node.js)             │
│  • Fastify / Express                    │
│  • Prisma ORM + PostgreSQL              │
│  • Redis (caching)                      │
│  • BullMQ (job queue)                   │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│           AI SERVICES                 │
│  • OpenAI GPT-4 / Claude / Gemini     │
│  • Fine-tuned model for Vietnamese    │
│  • Content moderation API             │
│  • Vector DB (Pinecone) for RAG       │
└─────────────────────────────────────────┘
```

#### 3.2 Database Schema

sql Copy

```sql
-- Users
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(255) UNIQUE,
    subscription_tier ENUM('free', 'pro', 'enterprise'),
    created_at TIMESTAMP
);

-- Projects
CREATE TABLE projects (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    title VARCHAR(255),
    original_outline TEXT,
    parsed_structure JSONB,
    content_rating VARCHAR(50),
    selected_mode VARCHAR(50),
    status ENUM('parsing', 'writing', 'review', 'completed'),
    created_at TIMESTAMP
);

-- Chapters
CREATE TABLE chapters (
    id UUID PRIMARY KEY,
    project_id UUID REFERENCES projects(id),
    chapter_number INT,
    title VARCHAR(255),
    outline_summary TEXT,
    generated_content TEXT,
    word_count INT,
    ai_model VARCHAR(50),
    generation_params JSONB
);

-- Content Safety Logs
CREATE TABLE safety_logs (
    id UUID PRIMARY KEY,
    project_id UUID REFERENCES projects(id),
    detected_flag VARCHAR(100),
    severity ENUM('low', 'medium', 'high', 'critical'),
    original_snippet TEXT,
    rewritten_snippet TEXT,
    user_approved BOOLEAN
);
```

***

### 4. API ENDPOINTS

#### 4.1 Core APIs

Table Copy

| Method | Endpoint                       | Description       | Request                  | Response               |
| ------ | ------------------------------ | ----------------- | ------------------------ | ---------------------- |
| POST   | `/api/projects`                | Tạo project mới   | `{file, title}`          | `{project_id, status}` |
| GET    | `/api/projects/:id/parse`      | Lấy kết quả parse | -                        | Parsed JSON            |
| POST   | `/api/projects/:id/write`      | Bắt đầu viết      | `{mode, options}`        | `{job_id}`             |
| GET    | `/api/jobs/:id/status`         | Check tiến độ     | -                        | `{status, progress}`   |
| GET    | `/api/chapters/:id`            | Lấy chapter       | -                        | Full content           |
| POST   | `/api/chapters/:id/regenerate` | Viết lại scene    | `{scene_id, new_prompt}` | New content            |

#### 4.2 WebSocket Events

JavaScript Copy

```javascript
// Real-time writing progress
socket.emit('subscribe', { project_id: 'xxx' });

socket.on('chapter_progress', (data) => {
  // { chapter: 3, total: 20, status: 'writing', word_count: 1500 }
});

socket.on('content_warning', (data) => {
  // { scene_id: '5.2', flag: 'explicit_content', options: [...] }
});
```

***

### 5. PROMPT ENGINEERING

#### 5.1 System Prompt Template

Python Copy

```python
VIETNAMESE_STORY_WRITER = """
Bạn là nhà văn chuyên nghiệp viết truyện tiếng Việt. Nhiệm vụ: Viết truyện dựa trên outline được cung cấp.

QUY TẮC TUYỆT ĐỐI:
1. KHÔNG viết nội dung khiêu dâm, tình dục rõ ràng, bạo lực cực đoan
2. KHÔNG mô tả chi tiết cơ thể nhạy cảm với mục đích kích thích
3. KHÔNG viết về quan hệ tình dục dù bằng ngôn ngữ ẩn dụ hay trực tiếp
4. Có thể viết về tình yêu, rung động, hôn nhẹ nhàng nếu phù hợp mode

MODE VIẾT: {writing_mode}

HƯỚNG DẪN THEO MODE:
- romance-pure: Tập trung cảm xúc, nội tâm, ngụ ý. Cảnh thân mật = nụ hôn, ôm ấp, lời yêu thương
- drama: Xung đột, phát triển nhân vật, đối thoại sâu sắc
- rom-com: Tình huống hài hước, nhịp điệu nhanh, dialog witty

NGÔN NGỮ: Tiếng Việt phong phú, câu văn uyển chuyển, miêu tả giác quan (thị giác, thính giác, xúc giác, khứu giác) thay vì mô tả cơ thể.

CẤU TRÚC CHAPTER:
1. Mở đầu: Bối cảnh, không khí
2. Diễn biến: Hành động, đối thoại, cảm xúc nhân vật
3. Cao trào: Khoảnh khắc quan trọng (theo mode)
4. Kết: Chuyển tiếp hoặc kết thúc chapter

OUTLINE CHAPTER CẦN VIẾT:
{chapter_outline}

VIẾT {word_count} TỪ CHO CHAPTER NÀY.
"""
```

#### 5.2 Scene Transformation Prompt

Python Copy

```python
TRANSFORM_EXPLICIT_SCENE = """
Cảnh gốc chứa nội dung nhạy cảm. Chuyển đổi sang phiên bản {target_mode}:

NGUYÊN TẮC CHUYỂN ĐỔI:
1. Giữ nguyên ý nghĩa cảm xúc (rung động, gần gũi, xung đột)
2. Thay thế hành động thân mật bằng:
   - Ánh mắt, biểu cảm
   - Lời nói chưa nói hết
   - Không gian im lặng đầy ý nghĩa
   - Hành động tượng trưng (nắm tay, chạm má, ôm nhẹ)
3. Tăng cường nội tâm: suy nghĩ, giằng xé, khao khát nhưng kiềm chế

VÍ DỤ:
- "Tay anh chạm vào..." → "Ánh mắt anh dừng lại nơi không nên, cả hai đều nghe tiếng tim mình"
- "Họ làm tình trong phòng trực" → "Họ trao nhau nụ hôn đầu tiên dưới ánh đèn khẩn cấp, bên ngoài mưa rơi không ngớt"

CẢNH CẦN CHUYỂN ĐỔI:
{original_scene}

VIẾT LẠI:
"""
```

***

### 6. GIAO DIỆN NGƯỜI DÙNG

#### 6.1 Wireframes

plain Copy

```
┌─────────────────────────────────────────────────────────┐
│  [Logo]  Dashboard | My Stories | Templates | Settings   [User] │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌─────────────────┐  ┌─────────────────────────────┐   │
│  │  UPLOAD OUTLINE │  │      PROJECT SETTINGS       │   │
│  │                 │  │                             │   │
│  │  [Drop zone]    │  │  Title: _________________     │   │
│  │  PDF/DOCX/TXT   │  │                             │   │
│  │                 │  │  Writing Mode:              │   │
│  │  [Upload btn]   │  │  ○ Romance-Pure             │   │
│  │                 │  │  ○ Drama                    │   │
│  │  Preview:       │  │  ○ Rom-Com                  │   │
│  │  ┌─────────┐    │  │  ○ Thriller                 │   │
│  │  │Parsed   │    │  │                             │   │
│  │  │Structure│    │  │  Target Length: [___] words │   │
│  │  └─────────┘    │  │                             │   │
│  │                 │  │  [Start Writing →]            │   │
│  └─────────────────┘  └─────────────────────────────┘   │
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │              WRITING PROGRESS                     │    │
│  │  ████████████████████░░░░░  Chapter 8/20         │    │
│  │  Current: "Cơn mưa đầu mùa" - 1,850 words       │    │
│  │  ETA: 12 minutes remaining                        │    │
│  │                                                 │    │
│  │  [⚠️ Content Warning] Scene 5.2 requires review   │    │
│  │      [Review & Approve] [Auto-rewrite]          │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
└─────────────────────────────────────────────────────────┘
```

#### 6.2 Chapter Editor View

plain Copy

```
┌─────────────────────────────────────────────────────────┐
│  ← Back to Project    Chapter 5: Những rung động...    [✓] │
├─────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌──────────────────────────────────┐   │
│  │   OUTLINE   │  │      GENERATED CONTENT           │   │
│  │             │  │                                  │   │
│  │ Scene 5.1   │  │  Mưa bất chợt đổ xuống chiều    │   │
│  │ ━━━━━━━━━━  │  │  hôm ấy. Trinh đứng dưới mái    │   │
│  │ Scene 5.2 ⚠️│  │  hiên nhỏ của bệnh viện, nhìn   │   │
│  │ ━━━━━━━━━━  │  │  những hạt mưa bay ngang...     │   │
│  │ Scene 5.3   │  │                                  │   │
│  │             │  │  [Edit with AI]                  │   │
│  │             │  │  [Regenerate] [Change Tone]      │   │
│  │             │  │                                  │   │
│  │             │  │  Word count: 2,340 | Read time:  │   │
│  │             │  │  12 min                          │   │
│  └─────────────┘  └──────────────────────────────────┘   │
│                                                         │
│  [◀ Prev]  [Save Draft]  [Export]  [Next ▶]             │
└─────────────────────────────────────────────────────────┘
```

***

### 7. CONTENT SAFETY SYSTEM

#### 7.1 Detection Categories

Table Copy

| Category            | Description                       | Action                  |
| ------------------- | --------------------------------- | ----------------------- |
| **Explicit Sexual** | Mô tả chi tiết hành vi tình dục   | Block + Suggest rewrite |
| **Suggestive**      | Ngụ ý tình dục rõ ràng            | Flag + User review      |
| **Violence**        | Bạo lực cực đoan, tra tấn         | Block                   |
| **Age Gap**         | Quan hệ chênh lệch tuổi (>10 năm) | Warning label           |
| **Power Imbalance** | Sếp-nhân viên, bác sĩ-bệnh nhân   | Ethical review          |

#### 7.2 User Control Flow

plain Copy

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Upload    │────▶│   Parse     │────▶│   Detect    │
│   Outline   │     │  Structure  │     │   Flags     │
└─────────────┘     └─────────────┘     └──────┬──────┘
                                               │
                          ┌────────────────────┼────────────────────┐
                          │                    │                    │
                          ▼                    ▼                    ▼
                    ┌─────────┐          ┌─────────┐           ┌─────────┐
                    │  SAFE   │          │ WARNING │           │ BLOCKED │
                    │         │          │         │           │         │
                    │ Continue│          │ Options:│           │ Must    │
                    │ writing │          │ • Rewrite│          │ modify  │
                    │         │          │ • Skip   │           │ outline │
                    │         │          │ • Manual │           │         │
                    └─────────┘          │   edit   │           └─────────┘
                                         └─────────┘
```

***

### 8. MONETIZATION & LIMITS

Table Copy

| Tier           | Price     | Features                                                          |
| -------------- | --------- | ----------------------------------------------------------------- |
| **Free**       | $0        | 3 projects/month, 5k words/project, basic modes                   |
| **Pro**        | $9.99/mo  | Unlimited projects, 50k words/project, all modes, export EPUB/PDF |
| **Enterprise** | $49.99/mo | API access, custom fine-tuning, white-label, priority support     |

***

### 9. IMPLEMENTATION ROADMAP

Table Copy

| Phase    | Duration | Deliverables                                                        |
| -------- | -------- | ------------------------------------------------------------------- |
| **MVP**  | 6 weeks  | Upload, parse, basic writing (romance-pure, drama), web reader      |
| **v1.0** | +4 weeks | All modes, export formats, payment integration                      |
| **v1.5** | +6 weeks | Collaborative editing, AI image generation for covers, audio export |
| **v2.0** | +8 weeks | Mobile app, community features, publishing to platform              |

***

### 10. RISK MITIGATION

Table Copy

| Risk                                    | Mitigation                                                                |
| --------------------------------------- | ------------------------------------------------------------------------- |
| AI generates inappropriate content      | Multi-layer filtering: model-level + post-processing + human review queue |
| Copyright issues with uploaded outlines | Terms of service: user retains rights, we claim license to process        |
| Vietnamese language quality             | Fine-tune on Vietnamese literature corpus, human-in-the-loop validation   |
| Content policy violations               | Strict safety system, user education, clear guidelines                    |

***

Bạn cần tôi mở rộng phần nào? Ví dụ:

* Chi tiết kỹ thuật AI/ML pipeline
* API documentation đầy đủ
* Database schema chi tiết hơn
* UI/UX mockup chi tiết


# PHẦN 1: API DOCUMENTATION ĐẦY ĐỦ

### **StoryForge API v1.0**

Base URL: `https://api.storyforge.ai/v1` Authentication: Bearer Token (JWT)

***

#### **1. AUTHENTICATION**

**POST /auth/register**

Đăng ký tài khoản mới.**Request:**&#x4A;SON Copy

```json
{
  "email": "user@example.com",
  "password": "SecurePass123!",
  "name": "Nguyễn Văn A"
}
```

**Response (201):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "user_id": "usr_8f7d2a9e1b3c",
    "email": "user@example.com",
    "name": "Nguyễn Văn A",
    "subscription_tier": "free",
    "tokens_remaining": 10000,
    "created_at": "2024-01-15T08:30:00Z"
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

**Error Codes:**

* `400`: Email đã tồn tại
* `422`: Password không đủ mạnh
* `429`: Quá nhiều request đăng ký

***

**POST /auth/login**

Đăng nhập.**Request:**&#x4A;SON Copy

```json
{
  "email": "user@example.com",
  "password": "SecurePass123!"
}
```

**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "user_id": "usr_8f7d2a9e1b3c",
    "email": "user@example.com",
    "name": "Nguyễn Văn A",
    "subscription_tier": "pro",
    "tokens_remaining": 45000,
    "projects_count": 12,
    "last_login": "2024-01-15T08:30:00Z"
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "rt_9a8b7c6d5e4f..."
}
```

***

**POST /auth/refresh**

Làm mới token.**Request:**&#x4A;SON Copy

```json
{
  "refresh_token": "rt_9a8b7c6d5e4f..."
}
```

**Response (200):**&#x4A;SON Copy

```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 3600
}
```

***

**POST /auth/logout**

Đăng xuất (vô hiệu hóa token).**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "message": "Logged out successfully"
}
```

***

#### **2. USER MANAGEMENT**

**GET /users/me**

Lấy thông tin user hiện tại.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "user_id": "usr_8f7d2a9e1b3c",
  "email": "user@example.com",
  "name": "Nguyễn Văn A",
  "avatar_url": "https://cdn.storyforge.ai/avatars/usr_8f7d.jpg",
  "subscription": {
    "tier": "pro",
    "started_at": "2024-01-01T00:00:00Z",
    "expires_at": "2024-12-31T23:59:59Z",
    "features": {
      "max_projects": -1,
      "max_words_per_project": 50000,
      "available_modes": ["romance-pure", "drama", "rom-com", "thriller", "slice-of-life"],
      "export_formats": ["epub", "pdf", "docx", "audio"],
      "priority_processing": true
    }
  },
  "usage": {
    "projects_this_month": 8,
    "words_generated_this_month": 125000,
    "tokens_remaining": 45000
  },
  "preferences": {
    "default_writing_mode": "romance-pure",
    "language": "vi",
    "email_notifications": true
  }
}
```

***

**PATCH /users/me**

Cập nhật thông tin.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "name": "Nguyễn Văn B",
  "preferences": {
    "default_writing_mode": "drama",
    "email_notifications": false
  }
}
```

**Response (200):** Updated user object

***

**POST /users/me/avatar**

Upload avatar.**Headers:**

* `Authorization: Bearer {token}`
* `Content-Type: multipart/form-data`

**Body:** `file` (image, max 5MB)**Response (200):**&#x4A;SON Copy

```json
{
  "avatar_url": "https://cdn.storyforge.ai/avatars/usr_8f7d_new.jpg"
}
```

***

#### **3. PROJECT MANAGEMENT**

**GET /projects**

Liệt kê tất cả projects.**Headers:** `Authorization: Bearer {token}`**Query Parameters:**&#x54;able Copy

| Param     | Type   | Description                     | Default     |
| --------- | ------ | ------------------------------- | ----------- |
| `page`    | int    | Trang hiện tại                  | 1           |
| `limit`   | int    | Số items/trang                  | 20          |
| `status`  | string | Lọc theo status                 | all         |
| `sort_by` | string | created\_at, updated\_at, title | updated\_at |
| `order`   | string | asc, desc                       | desc        |

**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "projects": [
      {
        "project_id": "prj_9a8b7c6d5e4f",
        "title": "Y tá nhỏ của tôi",
        "status": "completed",
        "content_rating": "R-18",
        "writing_mode": "romance-pure",
        "progress": {
          "chapters_total": 20,
          "chapters_completed": 20,
          "words_total": 45000
        },
        "created_at": "2024-01-10T08:00:00Z",
        "updated_at": "2024-01-15T14:30:00Z",
        "thumbnail_url": "https://cdn.storyforge.ai/covers/prj_9a8b.jpg"
      }
    ],
    "pagination": {
      "current_page": 1,
      "total_pages": 3,
      "total_items": 45,
      "has_next": true,
      "has_prev": false
    }
  }
}
```

***

**POST /projects**

Tạo project mới.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "title": "Y tá nhỏ của tôi",
  "outline_file": "[FILE_UPLOAD]", // multipart
  "writing_mode": "romance-pure",
  "options": {
    "target_word_count": 50000,
    "chapter_count": 20,
    "language": "vi",
    "tone": "warm,emotional",
    "pov": "third_person_limited",
    "target_audience": "young_adult"
  }
}
```

**Response (201):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "project_id": "prj_9a8b7c6d5e4f",
    "title": "Y tá nhỏ của tôi",
    "status": "uploading",
    "upload_url": "https://upload.storyforge.ai/temp/upl_12345",
    "estimated_processing_time": 30,
    "tokens_deducted": 500
  }
}
```

***

**GET /projects/:id**

Lấy chi tiết project.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "project_id": "prj_9a8b7c6d5e4f",
    "title": "Y tá nhỏ của tôi",
    "tagline": "Em 20, lần đầu biết yêu trong phòng trực đêm mưa...",
    "status": "completed",
    "content_rating": "R-18",
    "content_flags": ["age_gap_relationship", "workplace_romance"],
    "writing_mode": "romance-pure",
    "original_outline": {
      "file_url": "https://cdn.storyforge.ai/outlines/prj_9a8b_original.pdf",
      "text_excerpt": "CHƯƠNG 1: CÔ GÁI NHỎ VÀO KHOA NGOẠI..."
    },
    "parsed_structure": {
      "metadata": { "...": "..." },
      "characters": [ "..." ],
      "chapters": [ "..." ],
      "sensory_elements": { "..." }
    },
    "progress": {
      "chapters_total": 20,
      "chapters_completed": 20,
      "chapters_in_review": 0,
      "words_total": 45230,
      "estimated_completion": null
    },
    "chapters": [
      {
        "chapter_id": "chap_001",
        "chapter_number": 1,
        "title": "Cô gái nhỏ vào khoa Ngoại",
        "status": "completed",
        "word_count": 2150,
        "preview": "Bối cảnh: Sáng sớm, bệnh viện tỉnh..."
      }
    ],
    "safety_logs": [
      {
        "log_id": "sfl_001",
        "scene_id": "1.1",
        "flag": "suggestive_attire_description",
        "severity": "low",
        "action_taken": "rewritten",
        "user_approved": true
      }
    ],
    "created_at": "2024-01-10T08:00:00Z",
    "updated_at": "2024-01-15T14:30:00Z",
    "completed_at": "2024-01-15T14:30:00Z"
  }
}
```

***

**PATCH /projects/:id**

Cập nhật project.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "title": "Y tá nhỏ của tôi - Bản chỉnh sửa",
  "writing_mode": "drama",
  "options": {
    "tone": "melancholic,intense"
  }
}
```

**Lưu ý:** Không thể đổi writing\_mode nếu đã bắt đầu viết.

***

**DELETE /projects/:id**

Xóa project.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "message": "Project deleted successfully",
  "refunded_tokens": 200
}
```

***

**POST /projects/:id/duplicate**

Nhân bản project.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "new_title": "Y tá nhỏ của tôi - Phiên bản Drama",
  "writing_mode": "drama"
}
```

**Response (201):** New project object

***

#### **4. OUTLINE PARSING**

**GET /projects/:id/parse**

Lấy kết quả parse outline.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "parse_status": "completed",
    "parsed_at": "2024-01-10T08:05:00Z",
    "structure": {
      "metadata": {
        "title": "Y tá nhỏ của tôi",
        "tagline": "Em 20, lần đầu biết yêu...",
        "detected_genres": ["romance", "medical", "drama"],
        "content_rating": "R-18",
        "confidence_score": 0.94
      },
      "characters": [
        {
          "character_id": "char_001",
          "name": "Trinh",
          "age": 20,
          "role": "protagonist",
          "occupation": "y tá",
          "personality_traits": ["ngây thơ", "chăm chỉ", "nhạy cảm"],
          "physical_description": "Dáng thanh mảnh, da trắng hồng, mắt bồ câu trong veo",
          "character_arc": "Từ ngây thơ đến trưởng thành qua tình yêu",
          "relationships": [
            {
              "with_character": "An",
              "relationship_type": "love_interest",
              "dynamic": "mentor-mentee, age_gap"
            }
          ]
        },
        {
          "character_id": "char_002",
          "name": "An",
          "age": 35,
          "role": "deuteragonist",
          "occupation": "bác sĩ phẫu thuật",
          "personality_traits": ["lạnh lùng", "tài giỏi", "cô đơn"],
          "backstory": "Từng thất bại trong tình yêu, đóng kín trái tim",
          "character_arc": "Mở lòng và yêu lại"
        }
      ],
      "chapters": [
        {
          "chapter_id": "chap_001",
          "chapter_number": 1,
          "title": "Cô gái nhỏ vào khoa Ngoại",
          "setting": {
            "location": "Bệnh viện tỉnh, khoa Ngoại",
            "time": "Sáng sớm",
            "atmosphere": "hối hả, mới mẻ"
          },
          "scenes": [
            {
              "scene_id": "1.1",
              "sequence": 1,
              "location": "Phòng thay đồ nữ",
              "characters_present": ["Trinh"],
              "action_summary": "Trinh chuẩn bị đi làm ngày đầu tiên, lo lắng nhưng tự tin",
              "key_moments": [
                "Trinh ngắm mình trong gương với đồng phục mới",
                "Mẹ gọi video động viên",
                "Trinh tự nhủ phải cố gắng"
              ],
              "sensory_details": {
                "visual": ["màu xanh nhạt của đồng phục", "ánh sáng vàng của đèn phòng thay đồ"],
                "tactile": ["vải đồng phục hơi chật", "da mịn sau khi tắm"],
                "auditory": ["tiếng điện thoại rung", "tiếng cười của mẹ qua video"]
              },
              "emotional_tone": "hồi hộp, hy vọng",
              "estimated_word_count": 800,
              "content_flags": ["suggestive_attire"]
            }
          ],
          "chapter_arc": "Introduction - Trinh bước vào thế giới mới"
        }
      ],
      "themes": ["tình yêu chênh lệch tuổi", "trưởng thành", "chữa lành"],
      "motifs": ["mưa", "ánh sáng và bóng tối", "màu trắng của y tế"],
      "sensory_palette": {
        "dominant_colors": ["trắng", "xanh nhạt", "xám"],
        "dominant_sounds": ["tiếng bước chân", "tiếng máy móc", "tiếng mưa"],
        "dominant_scents": ["mùi bạc hà", "mùi thuốc sát trùng", "mùi mưa"]
      }
    },
    "suggestions": [
      {
        "type": "character_development",
        "message": "Có thể thêm backstory về gia đình Trinh để tăng chiều sâu"
      },
      {
        "type": "pacing",
        "message": "Chương 5-6 có nhiều cảnh tình cảm liên tiếp, nên xen kẽ cảnh y tế"
      }
    ]
  }
}
```

***

**POST /projects/:id/parse/retry**

Yêu cầu parse lại nếu lỗi.**Headers:** `Authorization: Bearer {token}`**Response (202):** Accepted, processing

***

#### **5. WRITING ENGINE**

**POST /projects/:id/write**

Bắt đầu quá trình viết.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "chapters": ["all"], // hoặc ["chap_001", "chap_002"]
  "writing_mode": "romance-pure",
  "options": {
    "style_preset": "contemporary_vietnamese",
    "sentence_complexity": "medium",
    "dialogue_ratio": 0.3,
    "description_detail": "rich",
    "emotional_depth": "deep",
    "pacing": "moderate"
  },
  "priority": "normal" // hoặc "high" (Pro+)
}
```

**Response (202):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "job_id": "job_9a8b7c6d5e4f",
    "status": "queued",
    "estimated_duration": 1800,
    "chapters_queued": 20,
    "websocket_channel": "ws://ws.storyforge.ai/projects/prj_9a8b"
  }
}
```

***

**GET /jobs/:id**

Kiểm tra tiến độ writing job.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "job_id": "job_9a8b7c6d5e4f",
    "status": "processing", // queued, processing, completed, failed, paused
    "progress": {
      "total_chapters": 20,
      "completed_chapters": 8,
      "current_chapter": {
        "chapter_id": "chap_009",
        "chapter_number": 9,
        "title": "Thang máy và hầm xe",
        "status": "writing",
        "words_written": 1200,
        "estimated_remaining": 300
      },
      "overall_percentage": 45,
      "elapsed_time": 420,
      "estimated_remaining_time": 510
    },
    "logs": [
      {
        "timestamp": "2024-01-10T08:15:00Z",
        "level": "info",
        "message": "Started chapter 9: Thang máy và hầm xe"
      },
      {
        "timestamp": "2024-01-10T08:18:00Z",
        "level": "warning",
        "message": "Content flag detected in scene 9.4, auto-rewriting",
        "flag": {
          "type": "suggestive_content",
          "scene_id": "9.4",
          "action": "auto_rewritten"
        }
      }
    ]
  }
}
```

***

**POST /jobs/:id/pause**

Tạm dừng job.**Headers:** `Authorization: Bearer {token}`**Response (200):** Updated job status

***

**POST /jobs/:id/resume**

Tiếp tục job.**Headers:** `Authorization: Bearer {token}`

***

**POST /jobs/:id/cancel**

Hủy job.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "refunded_tokens": 1500,
  "completed_chapters_saved": 8
}
```

***

#### **6. CHAPTER MANAGEMENT**

**GET /chapters/:id**

Lấy nội dung chapter.**Headers:** `Authorization: Bearer {token}`**Query Parameters:**&#x54;able Copy

| Param               | Type    | Default                            |
| ------------------- | ------- | ---------------------------------- |
| `format`            | string  | json (json, html, markdown, plain) |
| `include_outline`   | boolean | false                              |
| `include_revisions` | boolean | false                              |

**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "chapter_id": "chap_009",
    "project_id": "prj_9a8b7c6d5e4f",
    "chapter_number": 9,
    "title": "Thang máy và hầm xe",
    "status": "completed",
    "content": {
      "full_text": "Chiều muộn, bệnh viện bắt đầu vắng bóng người...",
      "word_count": 2450,
      "reading_time": 12,
      "paragraphs": [
        {
          "index": 1,
          "text": "Chiều muộn, bệnh viện bắt đầu vắng bóng người...",
          "type": "narration",
          "sentiment": "neutral"
        },
        {
          "index": 2,
          "text": "\"Em đi sinh nhật bạn à?\" An hỏi, giọng khàn hơn thường lệ.",
          "type": "dialogue",
          "speaker": "An",
          "sentiment": "curious"
        }
      ]
    },
    "outline": {
      "scene_id": "9.1",
      "original_summary": "Trinh chuẩn bị đi sinh nhật, gặp An ở hành lang"
    },
    "revisions": [
      {
        "revision_id": "rev_001",
        "created_at": "2024-01-10T09:00:00Z",
        "change_type": "auto_rewrite",
        "reason": "Content safety: suggestive_description",
        "original_snippet": "Chiếc váy ôm sát lấy đường cong cơ thể...",
        "rewritten_snippet": "Chiếc váy màu pastel khiến Trinh trông thật khác, như một nàng thơ bước ra từ tranh vẽ..."
      }
    ],
    "metadata": {
      "ai_model": "gpt-4",
      "temperature": 0.7,
      "generation_time": 45,
      "tokens_used": 3500
    },
    "created_at": "2024-01-10T08:15:00Z",
    "updated_at": "2024-01-10T09:00:00Z"
  }
}
```

***

**PATCH /chapters/:id**

Cập nhật chapter (manual edit).**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "title": "Thang máy và hầm xe - Bản chỉnh sửa",
  "content": {
    "full_text": "Nội dung mới..."
  }
}
```

**Response (200):** Updated chapter

***

**POST /chapters/:id/regenerate**

Viết lại chapter/scene.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "scope": "full", // full, scene, paragraph
  "target": {
    "scene_id": "9.4", // nếu scope = scene
    // hoặc "paragraph_index": 5
  },
  "prompt": "Viết lại cảnh trong thang máy, tập trung vào xung đột nội tâm của An thay vì mô tả bề ngoài Trinh",
  "writing_mode": "drama", // có thể đổi mode
  "preserve_word_count": true
}
```

**Response (202):**&#x4A;SON Copy

```json
{
  "revision_job_id": "job_rev_001",
  "estimated_time": 120
}
```

***

**POST /chapters/:id/feedback**

Gửi feedback cho AI.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "rating": 4,
  "feedback_text": "Cảnh hội thoại hay nhưng miêu tả cảm xúc hơi sến súa",
  "specific_issues": [
    {
      "type": "too_melodramatic",
      "location": "paragraph 12-15"
    }
  ],
  "suggested_improvement": "Giảm bớt tính từ cảm xúc, tăng hành động cụ thể"
}
```

***

#### **7. CONTENT SAFETY**

**GET /projects/:id/safety-review**

Xem các cảnh cần review.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "pending_reviews": 3,
    "items": [
      {
        "review_id": "rev_safety_001",
        "chapter_id": "chap_009",
        "scene_id": "9.4",
        "location": {
          "paragraph_start": 15,
          "paragraph_end": 18
        },
        "flag_type": "suggestive_content",
        "severity": "medium",
        "ai_analysis": {
          "original_text": "An nhìn thấy đường cong lấp ló sau lớp vải mỏng...",
          "concern": "Mô tả nhắm đến cơ thể với tính chất kích thích",
          "suggested_rewrite": "An nhìn thấy Trinh thật khác lạ trong bộ váy mới, và anh buộc phải nhận ra mình đã chú ý đến cô nhiều hơn mức bác sĩ nên có..."
        },
        "options": [
          {
            "action": "accept_rewrite",
            "description": "Chấp nhận đề xuất viết lại",
            "preview": "An nhìn thấy Trinh thật khác lạ..."
          },
          {
            "action": "custom_rewrite",
            "description": "Tự viết lại",
            "custom_prompt": ""
          },
          {
            "action": "skip_scene",
            "description": "Bỏ qua cảnh này",
            "note": "Chapter sẽ ngắn hơn ~200 từ"
          },
          {
            "action": "accept_original",
            "description": "Giữ nguyên (yêu cầu xác nhận)",
            "requires_confirmation": true,
            "confirmation_reason": "Nội dung có thể vi phạm chính sách nền tảng"
          }
        ],
        "created_at": "2024-01-10T08:20:00Z",
        "expires_at": "2024-01-17T08:20:00Z"
      }
    ]
  }
}
```

***

**POST /safety-review/:id/resolve**

Giải quyết content flag.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "action": "accept_rewrite",
  "custom_text": null, // nếu action = custom_rewrite
  "confirmation_password": null // nếu action = accept_original
}
```

**Response (200):** Updated chapter

***

#### **8. EXPORT & DOWNLOAD**

**POST /projects/:id/export**

Xuất project.**Headers:** `Authorization: Bearer {token}`**Request:**&#x4A;SON Copy

```json
{
  "format": "epub", // epub, pdf, docx, txt, html, audio
  "options": {
    "include_cover": true,
    "cover_image": "[FILE_UPLOAD]", // hoặc auto-generate
    "include_toc": true,
    "include_metadata": true,
    "font_family": "Noto Serif", // PDF
    "font_size": 12,
    "line_spacing": 1.5,
    "page_size": "A5", // PDF
    "chapter_separator": "page_break",
    "header_template": "{title} - {author}",
    "footer_template": "{page_number}",
    "watermark": null, // hoặc text
    "drm": false // EPUB
  },
  "audio_options": { // nếu format = audio
    "voice": "vi-female-1",
    "speed": 1.0,
    "emotion": "warm",
    "background_music": "soft_piano",
    "split_by": "chapter"
  }
}
```

**Response (202):**&#x4A;SON Copy

```json
{
  "success": true,
  "data": {
    "export_job_id": "exp_001",
    "status": "processing",
    "estimated_time": 60,
    "download_url": null // sẽ có khi completed
  }
}
```

***

**GET /exports/:id/status**

Kiểm tra tiến độ export.**Headers:** `Authorization: Bearer {token}`**Response (200):**&#x4A;SON Copy

```json
{
  "export_job_id": "exp_001",
  "status": "completed",
  "download_url": "https://cdn.storyforge.ai/exports/prj_9a8b.epub?token=xyz",
  "expires_at": "2024-01-17T10:00:00Z",
  "file_size": 2450000,
  "checksum": "sha256:abc123..."
}
```

***

**GET /download/:token**

Download file (public URL với token tạm thời).**Query:** `token` từ download\_url**Response:** File binary với headers:plain Copy

```
Content-Type: application/epub+zip
Content-Disposition: attachment; filename="y-ta-nho-cua-toi.epub"
X-Content-Checksum: sha256:abc123...
```

***

#### **9. WEBSOCKET EVENTS**

**Connection:** `wss://ws.storyforge.ai/v1`**Authentication:** Query param `?token={jwt}`

**Client → Server Events**

Table Copy

| Event                 | Payload          | Description      |
| --------------------- | ---------------- | ---------------- |
| `subscribe_project`   | `{ project_id }` | Theo dõi project |
| `unsubscribe_project` | `{ project_id }` | Hủy theo dõi     |
| `ping`                | `{}`             | Keep-alive       |

**Server → Client Events**

Table Copy

| Event              | Payload                                         | Description        |
| ------------------ | ----------------------------------------------- | ------------------ |
| `connected`        | `{ session_id }`                                | Kết nối thành công |
| `project_update`   | `{ project_id, status, progress }`              | Cập nhật project   |
| `chapter_complete` | `{ chapter_id, chapter_number, word_count }`    | Chapter xong       |
| `content_warning`  | `{ review_id, chapter_id, scene_id, severity }` | Cần review         |
| `job_complete`     | `{ job_id, type, download_url? }`               | Job hoàn thành     |
| `error`            | `{ code, message, details }`                    | Lỗi                |

**Example WebSocket Flow:**&#x4A;avaScript Copy

```javascript
// Client
const ws = new WebSocket('wss://ws.storyforge.ai/v1?token=eyJhb...');

ws.onopen = () => {
  ws.send(JSON.stringify({
    event: 'subscribe_project',
    data: { project_id: 'prj_9a8b' }
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  
  switch(msg.event) {
    case 'chapter_complete':
      console.log(`Chapter ${msg.data.chapter_number} done!`);
      break;
    case 'content_warning':
      showReviewModal(msg.data);
      break;
  }
};
```

***

#### **10. ERROR HANDLING**

**Error Response Format**

JSON Copy

```json
{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_TOKENS",
    "message": "Not enough tokens to complete this request",
    "details": {
      "required": 5000,
      "available": 2300,
      "suggested_action": "upgrade_subscription"
    },
    "request_id": "req_9a8b7c6d5e4f",
    "timestamp": "2024-01-10T08:30:00Z"
  }
}
```

**Error Codes**

Table Copy

| Code                       | HTTP Status | Description                | Resolution              |
| -------------------------- | ----------- | -------------------------- | ----------------------- |
| `UNAUTHORIZED`             | 401         | Token hết hạn/không hợp lệ | Refresh hoặc login lại  |
| `FORBIDDEN`                | 403         | Không đủ quyền             | Upgrade subscription    |
| `NOT_FOUND`                | 404         | Resource không tồn tại     | Kiểm tra ID             |
| `VALIDATION_ERROR`         | 422         | Dữ liệu không hợp lệ       | Kiểm tra schema         |
| `RATE_LIMITED`             | 429         | Quá nhiều request          | Chờ hoặc upgrade        |
| `INSUFFICIENT_TOKENS`      | 402         | Hết tokens                 | Mua thêm hoặc chờ reset |
| `AI_SERVICE_UNAVAILABLE`   | 503         | AI service đang bảo trì    | Thử lại sau             |
| `CONTENT_POLICY_VIOLATION` | 400         | Outline vi phạm chính sách | Sửa outline             |
| `FILE_TOO_LARGE`           | 413         | File > 50MB                | Nén hoặc chia nhỏ       |
| `UNSUPPORTED_FORMAT`       | 415         | Định dạng không hỗ trợ     | Chuyển đổi format       |

***

#### **11. RATE LIMITING**

Table Copy

| Tier       | Requests/minute | Concurrent jobs | Max file size |
| ---------- | --------------- | --------------- | ------------- |
| Free       | 30              | 1               | 10MB          |
| Pro        | 120             | 3               | 50MB          |
| Enterprise | 600             | 10              | 200MB         |

**Headers trong mọi response:**&#x70;lain Copy

```
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 115
X-RateLimit-Reset: 1704892200
```

***

## **PHẦN 2: DATABASE SCHEMA CHI TIẾT**

### **PostgreSQL Schema**

sql Copy

```sql
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For text search

-- Enums
CREATE TYPE user_tier AS ENUM ('free', 'pro', 'enterprise');
CREATE TYPE project_status AS ENUM ('draft', 'uploading', 'parsing', 'parsed', 'writing', 'reviewing', 'completed', 'archived', 'deleted');
CREATE TYPE chapter_status AS ENUM ('pending', 'writing', 'completed', 'revision_pending', 'approved');
CREATE TYPE writing_mode AS ENUM ('romance_pure', 'romance_steamy', 'drama', 'rom_com', 'thriller', 'slice_of_life', 'custom');
CREATE TYPE content_flag_severity AS ENUM ('info', 'low', 'medium', 'high', 'critical');
CREATE TYPE job_status AS ENUM ('queued', 'processing', 'completed', 'failed', 'paused', 'cancelled');
CREATE TYPE export_format AS ENUM ('epub', 'pdf', 'docx', 'txt', 'html', 'audio_mp3', 'audio_m4b');

-- ============================================
-- USERS & AUTHENTICATION
-- ============================================

CREATE TABLE users (
    user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL, -- bcrypt
    name VARCHAR(100) NOT NULL,
    avatar_url VARCHAR(500),
    
    -- Subscription
    subscription_tier user_tier DEFAULT 'free',
    subscription_started_at TIMESTAMP,
    subscription_expires_at TIMESTAMP,
    
    -- Usage tracking
    tokens_balance INTEGER DEFAULT 10000,
    tokens_used_total BIGINT DEFAULT 0,
    tokens_used_this_month INTEGER DEFAULT 0,
    tokens_reset_at TIMESTAMP, -- Monthly reset
    
    -- Limits
    max_projects INTEGER DEFAULT 3,
    max_words_per_project INTEGER DEFAULT 5000,
    
    -- Preferences
    preferences JSONB DEFAULT '{
        "default_writing_mode": "romance_pure",
        "language": "vi",
        "email_notifications": true,
        "theme": "light"
    }'::jsonb,
    
    -- Security
    email_verified BOOLEAN DEFAULT false,
    email_verified_at TIMESTAMP,
    last_login_at TIMESTAMP,
    last_login_ip INET,
    failed_login_attempts INTEGER DEFAULT 0,
    locked_until TIMESTAMP,
    
    -- Timestamps
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP -- Soft delete
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_subscription ON users(subscription_tier, subscription_expires_at);

-- Refresh tokens
CREATE TABLE refresh_tokens (
    token_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    token_hash VARCHAR(255) NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    revoked_at TIMESTAMP,
    replaced_by UUID REFERENCES refresh_tokens(token_id),
    ip_address INET,
    user_agent TEXT
);

CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token_hash);

-- API keys (for enterprise)
CREATE TABLE api_keys (
    key_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    key_hash VARCHAR(255) NOT NULL,
    name VARCHAR(100),
    permissions JSONB DEFAULT '["read", "write"]'::jsonb,
    rate_limit INTEGER DEFAULT 600, -- per minute
    last_used_at TIMESTAMP,
    expires_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    revoked_at TIMESTAMP
);

-- ============================================
-- PROJECTS
-- ============================================

CREATE TABLE projects (
    project_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    
    -- Basic info
    title VARCHAR(255) NOT NULL,
    tagline VARCHAR(500),
    slug VARCHAR(300) UNIQUE, -- URL-friendly
    
    -- Status & workflow
    status project_status DEFAULT 'draft',
    writing_mode writing_mode DEFAULT 'romance_pure',
    
    -- Content analysis
    content_rating VARCHAR(10), -- G, PG, PG-13, R, R-18
    content_flags JSONB DEFAULT '[]'::jsonb, -- ["age_gap", "workplace_romance"]
    detected_genres JSONB DEFAULT '[]'::jsonb,
    
    -- Source files
    original_outline_url VARCHAR(500),
    original_outline_text TEXT, -- Extracted text
    original_filename VARCHAR(255),
    original_file_size INTEGER,
    original_file_hash VARCHAR(64), -- SHA-256
    
    -- Parsed structure
    parsed_structure JSONB, -- Full parsed outline
    parsed_at TIMESTAMP,
    parse_version INTEGER DEFAULT 1, -- Increment on re-parse
    
    -- Writing config
    writing_config JSONB DEFAULT '{
        "target_word_count": 50000,
        "chapter_count": 20,
        "language": "vi",
        "pov": "third_person_limited",
        "tone": "warm",
        "style_preset": "contemporary_vietnamese"
    }'::jsonb,
    
    -- Progress
    chapters_total INTEGER DEFAULT 0,
    chapters_completed INTEGER DEFAULT 0,
    words_total INTEGER DEFAULT 0,
    
    -- Current job
    current_job_id UUID,
    
    -- Metadata
    cover_image_url VARCHAR(500),
    thumbnail_url VARCHAR(500),
    
    -- Timestamps
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    started_writing_at TIMESTAMP,
    completed_at TIMESTAMP,
    archived_at TIMESTAMP,
    deleted_at TIMESTAMP
);

CREATE INDEX idx_projects_user ON projects(user_id, deleted_at);
CREATE INDEX idx_projects_status ON projects(status);
CREATE INDEX idx_projects_created ON projects(created_at DESC);

-- Project versions (for undo/backup)
CREATE TABLE project_versions (
    version_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    version_number INTEGER NOT NULL,
    snapshot JSONB NOT NULL, -- Full project snapshot
    created_by UUID REFERENCES users(user_id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    reason VARCHAR(255) -- "manual_save", "auto_save", "pre_regenerate"
);

CREATE UNIQUE INDEX idx_project_versions_number ON project_versions(project_id, version_number);

-- ============================================
-- CHARACTERS (Extracted from outline)
-- ============================================

CREATE TABLE characters (
    character_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    
    name VARCHAR(100) NOT NULL,
    aliases JSONB DEFAULT '[]'::jsonb, -- Other names they're called
    
    -- Basic attributes
    age INTEGER,
    age_approximate BOOLEAN DEFAULT false, -- "khoảng 20"
    gender VARCHAR(20),
    occupation VARCHAR(100),
    
    -- Role in story
    role_type VARCHAR(50), -- protagonist, deuteragonist, antagonist, supporting
    importance INTEGER DEFAULT 3, -- 1-5 scale
    
    -- Descriptions
    physical_description TEXT,
    personality_traits JSONB DEFAULT '[]'::jsonb,
    backstory TEXT,
    motivations JSONB DEFAULT '[]'::jsonb,
    fears JSONB DEFAULT '[]'::jsonb,
    
    -- Arc
    character_arc TEXT,
    arc_status VARCHAR(50), -- incomplete, developing, completed
    
    -- AI-generated enrichments
    ai_suggested_traits JSONB,
    ai_suggested_backstory TEXT,
    user_approved_enrichments BOOLEAN DEFAULT false,
    
    -- Tracking
    first_appearance_chapter INTEGER,
    appearance_count INTEGER DEFAULT 0,
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_characters_project ON characters(project_id);
CREATE INDEX idx_characters_role ON characters(project_id, role_type);

-- Character relationships
CREATE TABLE character_relationships (
    relationship_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    character_a_id UUID NOT NULL REFERENCES characters(character_id) ON DELETE CASCADE,
    character_b_id UUID NOT NULL REFERENCES characters(character_id) ON DELETE CASCADE,
    
    relationship_type VARCHAR(50), -- love_interest, family, colleague, enemy
    dynamic_description TEXT, -- "mentor-mentee with romantic tension"
    tension_level INTEGER, -- 1-10
    evolution JSONB, -- How relationship changes over chapters
    
    UNIQUE(character_a_id, character_b_id, relationship_type)
);

-- ============================================
-- CHAPTERS
-- ============================================

CREATE TABLE chapters (
    chapter_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    
    chapter_number INTEGER NOT NULL,
    title VARCHAR(255) NOT NULL,
    
    status chapter_status DEFAULT 'pending',
    
    -- Structure from outline
    outline_summary TEXT,
    setting_location VARCHAR(255),
    setting_time VARCHAR(100),
    atmosphere VARCHAR(100),
    chapter_arc VARCHAR(100), -- setup, rising_action, climax, falling_action, resolution
    
    -- Content
    content_full_text TEXT,
    content_html TEXT, -- Formatted version
    content_markdown TEXT,
    
    -- Breakdown
    paragraphs JSONB DEFAULT '[]'::jsonb, -- Array of paragraph objects
    scenes JSONB DEFAULT '[]'::jsonb, -- Scene breakdown
    
    -- Metrics
    word_count INTEGER DEFAULT 0,
    reading_time_minutes INTEGER,
    
    -- Sentiment analysis
    dominant_sentiment VARCHAR(20),
    sentiment_curve JSONB, -- Sentiment over paragraphs
    
    -- Generation metadata
    generation_params JSONB,
    ai_model VARCHAR(50),
    ai_temperature DECIMAL(3,2),
    tokens_used INTEGER,
    generation_duration_seconds INTEGER,
    generation_attempts INTEGER DEFAULT 1,
    
    -- Timestamps
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    generated_at TIMESTAMP,
    last_edited_at TIMESTAMP,
    approved_at TIMESTAMP
);

CREATE UNIQUE INDEX idx_chapters_project_number ON chapters(project_id, chapter_number);
CREATE INDEX idx_chapters_status ON chapters(status);

-- Chapter revisions (history)
CREATE TABLE chapter_revisions (
    revision_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    chapter_id UUID NOT NULL REFERENCES chapters(chapter_id) ON DELETE CASCADE,
    revision_number INTEGER NOT NULL,
    
    -- Content snapshot
    title VARCHAR(255),
    content_full_text TEXT,
    word_count INTEGER,
    
    -- Change tracking
    change_type VARCHAR(50), -- initial, manual_edit, ai_regenerate, auto_rewrite
    change_reason TEXT,
    triggered_by VARCHAR(50), -- user, system, safety_flag
    
    -- If AI-generated
    ai_prompt TEXT,
    ai_completion TEXT,
    
    -- If safety-related
    safety_review_id UUID,
    
    -- User who made change
    created_by UUID REFERENCES users(user_id), -- NULL if system
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    -- Diff from previous (for display)
    diff_added TEXT,
    diff_removed TEXT
);

CREATE UNIQUE INDEX idx_chapter_revisions_number ON chapter_revisions(chapter_id, revision_number);

-- ============================================
-- SCENES (Granular content tracking)
-- ============================================

CREATE TABLE scenes (
    scene_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    chapter_id UUID NOT NULL REFERENCES chapters(chapter_id) ON DELETE CASCADE,
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    
    scene_number INTEGER NOT NULL,
    outline_scene_id VARCHAR(50), -- Reference back to parsed outline
    
    -- Location
    location VARCHAR(255),
    time_of_day VARCHAR(50),
    
    -- Characters present
    characters_present JSONB DEFAULT '[]'::jsonb, -- ["char_001", "char_002"]
    
    -- Content
    summary TEXT, -- From outline
    generated_content TEXT,
    
    -- Position in chapter
    start_paragraph INTEGER,
    end_paragraph INTEGER,
    word_count INTEGER,
    
    -- Sensory elements
    sensory_visual JSONB,
    sensory_auditory JSONB,
    sensory_tactile JSONB,
    sensory_olfactory JSONB,
    
    -- Emotional
    emotional_tone VARCHAR(50),
    emotional_intensity INTEGER, -- 1-10
    
    -- Safety
    content_flags JSONB DEFAULT '[]'::jsonb,
    safety_review_status VARCHAR(50) DEFAULT 'none', -- none, pending, resolved
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_scenes_chapter ON scenes(chapter_id, scene_number);

-- ============================================
-- WRITING JOBS (Async processing)
-- ============================================

CREATE TABLE writing_jobs (
    job_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    
    status job_status DEFAULT 'queued',
    priority INTEGER DEFAULT 5, -- 1-10, higher = more priority
    
    -- Job config
    job_type VARCHAR(50), -- full_project, single_chapter, scene_rewrite, revision
    target_chapters JSONB, -- ["chap_001", "chap_002"] or ["all"]
    writing_mode writing_mode,
    generation_options JSONB,
    
    -- Progress
    total_chapters INTEGER,
    completed_chapters INTEGER DEFAULT 0,
    current_chapter_id UUID,
    
    -- Timing
    estimated_duration_seconds INTEGER,
    started_at TIMESTAMP,
    completed_at TIMESTAMP,
    failed_at TIMESTAMP,
    failure_reason TEXT,
    
    -- Resource tracking
    tokens_allocated INTEGER,
    tokens_consumed INTEGER DEFAULT 0,
    
    -- WebSocket
    websocket_channel VARCHAR(100),
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_writing_jobs_status ON writing_jobs(status, priority DESC, created_at);
CREATE INDEX idx_writing_jobs_project ON writing_jobs(project_id);

-- Job logs (detailed progress)
CREATE TABLE writing_job_logs (
    log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    job_id UUID NOT NULL REFERENCES writing_jobs(job_id) ON DELETE CASCADE,
    
    log_level VARCHAR(20), -- info, warning, error
    message TEXT NOT NULL,
    
    -- Context
    chapter_id UUID,
    scene_id VARCHAR(50),
    
    -- Additional data
    metadata JSONB,
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_job_logs_job ON writing_job_logs(job_id, created_at);

-- ============================================
-- CONTENT SAFETY
-- ============================================

CREATE TABLE safety_flags (
    flag_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    chapter_id UUID REFERENCES chapters(chapter_id),
    scene_id UUID REFERENCES scenes(scene_id),
    
    -- Detection
    detected_by VARCHAR(50), -- ai_model, human_review, automated
    detection_confidence DECIMAL(4,3), -- 0.000 - 1.000
    
    -- Classification
    flag_category VARCHAR(50), -- sexual_content, violence, hate_speech, etc.
    flag_type VARCHAR(100), -- specific subtype
    severity content_flag_severity,
    
    -- Location
    paragraph_index INTEGER,
    original_text_snippet TEXT,
    text_context TEXT, -- Surrounding text
    
    -- AI analysis
    ai_reasoning TEXT, -- Why AI flagged this
    suggested_rewrite TEXT,
    
    -- Resolution
    status VARCHAR(50) DEFAULT 'pending', -- pending, auto_resolved, user_resolved, escalated, dismissed
    resolution_action VARCHAR(50), -- accept_rewrite, custom_rewrite, skip_scene, accept_original
    resolved_text TEXT, -- Final text after resolution
    resolved_by UUID REFERENCES users(user_id),
    resolved_at TIMESTAMP,
    
    -- User interaction
    user_viewed_at TIMESTAMP,
    user_decision_notes TEXT,
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_safety_flags_project ON safety_flags(project_id, status);
CREATE INDEX idx_safety_flags_pending ON safety_flags(status, severity, created_at);

-- Safety rules (configurable)
CREATE TABLE safety_rules (
    rule_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(100) NOT NULL,
    description TEXT,
    
    -- Matching
    pattern_type VARCHAR(50), -- regex, keyword, semantic, combined
    pattern_definition JSONB, -- Pattern configuration
    
    -- Action
    default_action VARCHAR(50), -- flag, block, rewrite_auto
    severity_override content_flag_severity,
    
    -- Scope
    applies_to_modes JSONB DEFAULT '["all"]'::jsonb, -- Which writing modes
    user_tier_override JSONB, -- Different rules per tier
    
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ============================================
-- EXPORTS
-- ============================================

CREATE TABLE exports (
    export_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    project_id UUID NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    
    format export_format NOT NULL,
    status VARCHAR(50) DEFAULT 'processing', -- processing, completed, failed
    
    -- Config
    export_options JSONB NOT NULL,
    
    -- Result
    file_url VARCHAR(500),
    file_size_bytes BIGINT,
    file_checksum VARCHAR(64),
    download_token VARCHAR(255),
    download_expires_at TIMESTAMP,
    
    -- Processing
    processing_started_at TIMESTAMP,
    processing_completed_at TIMESTAMP,
    processing_duration_seconds INTEGER,
    
    error_message TEXT,
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_exports_project ON exports(project_id, created_at DESC);
CREATE INDEX idx_exports_download ON exports(download_token, download_expires_at);

-- ============================================
// ... (truncated for brevity, continuing with remaining tables)

CREATE TABLE user_feedback (
    feedback_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    project_id UUID REFERENCES projects(project_id),
    chapter_id UUID REFERENCES chapters(chapter_id),
    
    feedback_type VARCHAR(50), -- rating, text, issue_report
    rating INTEGER, -- 1-5
    feedback_text TEXT,
    
    -- Specific issues
    reported_issues JSONB, -- Array of issue objects
    
    -- AI improvement
    used_for_training BOOLEAN DEFAULT false,
    ai_model_version VARCHAR(50),
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Templates
CREATE TABLE templates (
    template_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(100) NOT NULL,
    description TEXT,
    
    category VARCHAR(50), -- romance, mystery, fantasy, etc.
    subcategory VARCHAR(50),
    
    -- Structure
    template_structure JSONB NOT NULL, -- Pre-defined outline structure
    
    -- Metadata
    is_official BOOLEAN DEFAULT false,
    created_by UUID REFERENCES users(user_id),
    usage_count INTEGER DEFAULT 0,
    average_rating DECIMAL(2,1),
    
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Notifications
CREATE TABLE notifications (
    notification_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    
    type VARCHAR(50), -- job_complete, content_warning, subscription_expiring
    title VARCHAR(255),
    message TEXT,
    
    -- Action
    action_url VARCHAR(500),
    action_text VARCHAR(100),
    
    -- Status
    is_read BOOLEAN DEFAULT false,
    read_at TIMESTAMP,
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_notifications_user ON notifications(user_id, is_read, created_at DESC);

-- Analytics events
CREATE TABLE analytics_events (
    event_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(user_id),
    anonymous_session_id VARCHAR(100),
    
    event_type VARCHAR(100) NOT NULL,
    event_properties JSONB,
    
    -- Context
    user_agent TEXT,
    ip_hash VARCHAR(64), -- Hashed for privacy
    country_code VARCHAR(2),
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) PARTITION BY RANGE (created_at);

-- Create monthly partitions for analytics
```

***

### **Redis Schema (Caching & Sessions)**

plain Copy

```
# User sessions
session:{jwt_token} -> {
    "user_id": "usr_xxx",
    "tier": "pro",
    "expires_at": 1704892200
}
TTL: 24 hours

# Rate limiting
rate_limit:{user_id}:{minute_timestamp} -> count
TTL: 2 minutes

# Project cache
project:{project_id}:metadata -> JSON
project:{project_id}:chapters -> JSON array
project:{project_id}:progress -> {
    "status": "writing",
    "completed": 8,
    "total": 20
}
TTL: 1 hour

# Job status (for WebSocket)
job:{job_id}:status -> JSON
job:{job_id}:logs -> List (LPUSH, trim to 100)
TTL: 24 hours after completion

# Export cache
export:{export_id}:status -> JSON
export:{export_id}:download_url -> string
TTL: 7 days

# AI generation cache (deduplication)
gen_hash:{sha256_of_prompt} -> {
    "result": "...",
    "model": "gpt-4",
    "created_at": 1704892200
}
TTL: 1 hour

# Real-time collaboration (if implemented)
doc:{chapter_id}:presence -> Hash of active users
doc:{chapter_id}:operations -> Stream of edits
```

***

## **PHẦN 3: UI/UX MOCKUP CHI TIẾT**

### **Design System**

#### **Color Palette**

css Copy

```css
:root {
  /* Primary */
  --primary-50: #eff6ff;
  --primary-100: #dbeafe;
  --primary-200: #bfdbfe;
  --primary-300: #93c5fd;
  --primary-400: #60a5fa;
  --primary-500: #3b82f6;  /* Main brand */
  --primary-600: #2563eb;
  --primary-700: #1d4ed8;
  --primary-800: #1e40af;
  --primary-900: #1e3a8a;
  
  /* Semantic */
  --success: #10b981;
  --warning: #f59e0b;
  --error: #ef4444;
  --info: #3b82f6;
  
  /* Content rating indicators */
  --rating-g: #22c55e;
  --rating-pg: #84cc16;
  --rating-pg13: #eab308;
  --rating-r: #f97316;
  --rating-r18: #dc2626;
  
  /* Neutral */
  --gray-50: #f9fafb;
  --gray-100: #f3f4f6;
  --gray-200: #e5e7eb;
  --gray-300: #d1d5db;
  --gray-400: #9ca3af;
  --gray-500: #6b7280;
  --gray-600: #4b5563;
  --gray-700: #374151;
  --gray-800: #1f2937;
  --gray-900: #111827;
  
  /* Typography */
  --font-sans: 'Inter', system-ui, sans-serif;
  --font-serif: 'Noto Serif', Georgia, serif;
  --font-mono: 'JetBrains Mono', monospace;
}
```

#### **Typography Scale**

Table Copy

| Token        | Size | Weight | Usage            |
| ------------ | ---- | ------ | ---------------- |
| `display-1`  | 48px | 700    | Hero titles      |
| `display-2`  | 36px | 700    | Page titles      |
| `heading-1`  | 30px | 600    | Section headers  |
| `heading-2`  | 24px | 600    | Card titles      |
| `heading-3`  | 20px | 600    | Subsection       |
| `body-large` | 18px | 400    | Lead paragraphs  |
| `body`       | 16px | 400    | Main text        |
| `body-small` | 14px | 400    | Secondary text   |
| `caption`    | 12px | 500    | Labels, metadata |

***

### **Screen Mockups**

#### **1. LANDING PAGE**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  [Logo: StoryForge]    Features  Pricing  Templates  [Sign In] [Get Started] │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                                                                     │   │
│  │     BIẾN Ý TƯỞNG THÀNH TRUYỆN HOÀN CHỈNH                           │   │
│  │     CHỈ TRONG VÀI PHÚT                                               │   │
│  │                                                                     │   │
│  │     Tải lên outline → AI phân tích → Chọn phong cách →             │   │
│  │     Nhận truyện chuyên nghiệp                                        │   │
│  │                                                                     │   │
│  │     [Bắt đầu viết miễn phí]  [Xem demo →]                            │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  TRUSTED BY 10,000+ NHÀ VĂN                                         │   │
│  │  [Logo Văn học trẻ] [Logo Wattpad VN] [Logo NXB Kim Đồng] ...        │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────┐  ┌─────────────────────┐  ┌─────────────────────┐ │
│  │   [Icon: Upload]    │  │   [Icon: Robot]     │  │   [Icon: Book]      │ │
│  │                     │  │                     │  │                     │ │
│  │  1. Tải Outline     │  │  2. AI Phân tích    │  │  3. Nhận Truyện     │ │
│  │                     │  │                     │  │                     │ │
│  │  PDF, DOCX, TXT     │  │  Nhân vật, cốt      │  │  EPUB, PDF, Audio   │ │
│  │  đều được           │  │  truyện, cảm xúc    │  │  xuất ra đọc ngay   │ │
│  │                     │  │                     │  │                     │ │
│  └─────────────────────┘  └─────────────────────┘  └─────────────────────┘ │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  CÁC THỂ LOẠI ĐƯỢC HỖ TRỢ                                          │   │
│  │                                                                     │   │
│  │  [Romance] [Drama] [Rom-Com] [Thriller] [Mystery] [Fantasy] ...     │   │
│  │                                                                     │   │
│  │  Mỗi thể loại có phong cách viết riêng, đảm bảo đúng "vibe"        │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  AN TOÀN & CHUYÊN NGHIỆP                                           │   │
│  │                                                                     │   │
│  │  ✓ Tự động phát hiện & chuyển đổi nội dung nhạy cảm                │   │
│  │  ✓ Nhiều chế độ: Tình cảm tinh khiết, Drama, Hài hước...           │   │
│  │  ✓ Quyền sở hữu 100% thuộc về bạn                                  │   │
│  │  ✓ Xuất đa định dạng: EPUB, PDF, DOCX, Audio                       │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  [Bắt đầu ngay — Miễn phí 10,000 tokens]                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **2. DASHBOARD**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  [≡]  [Logo]  [🔍 Search...]  [💬]  [🔔]  [👤 User ▼]                      │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌────────────┐                                                             │
│  │  NEW       │  Xin chào, Nguyễn Văn A! 👋                                  │
│  │  PROJECT   │  Bạn có 3 dự án đang viết dở                                │
│  │            │                                                             │
│  │  [➕]      │  ┌─────────────────────────────────────────────────────┐   │
│  │  Tạo      │  │  TIẾP TỤC VIẾT                                        │   │
│  │  dự án    │  │                                                       │   │
│  │  mới      │  │  ┌─────────────────────────────────────────────┐    │   │
│  │            │  │  │ [Cover: Y tá nhỏ...]  ████████████░░░░ 80%   │    │   │
│  │            │  │  │                       Chapter 16/20           │    │   │
│  │            │  │  │  "Y tá nhỏ của tôi"                           │    │   │
│  │            │  │  │  Romance • Đang viết • Còn ~15 phút          │    │   │
│  │            │  │  │  [Tiếp tục →]                                 │    │   │
│  │            │  │  └─────────────────────────────────────────────┘    │   │
│  │            │  │                                                       │   │
│  │            │  │  ┌─────────────────────────────────────────────┐    │   │
│  │            │  │  │ [Cover: Thám tử...]   ██████░░░░░░░░░░ 30%   │    │   │
│  │            │  │  │                       Chapter 6/20            │    │   │
│  │            │  │  │  "Thám tử Sài Gòn"                            │    │   │
│  │            │  │  │  Thriller • Tạm dừng                          │    │   │
│  │            │  │  │  [Tiếp tục →]                                 │    │   │
│  │            │  │  └─────────────────────────────────────────────┘    │   │
│  │            │  │                                                       │   │
│  └────────────┘  └─────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  DỰ ÁN GẦN ĐÂY                    [Xem tất cả →]                    │   │
│  │                                                                     │   │
│  │  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐              │   │
│  │  │ [Cover]│ │ [Cover]│ │ [Cover]│ │ [Cover]│ │ [Cover]│              │   │
│  │  │        │ │        │ │        │ │        │ │        │              │   │
│  │  │Completed│ │Completed│ │Review  │ │Draft   │ │Archived│              │   │
│  │  │Tình yêu │ │Hành trình│ │Pending │ │        │ │        │              │   │
│  │  │chị em   │ │về phía  │ │        │ │        │ │        │              │   │
│  │  │         │ │mặt trời │ │        │ │        │ │        │              │   │
│  │  │[⋮]     │ │[⋮]     │ │[⋮]     │ │[⋮]     │ │[⋮]     │              │   │
│  │  └────────┘ └────────┘ └────────┘ └────────┘ └────────┘              │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  THỐNG KÊ THÁNG NÀY                                                │   │
│  │                                                                     │   │
│  │  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐         │   │
│  │  │   45,230       │  │      3         │  │    12,500      │         │   │
│  │  │   từ đã viết   │  │   truyện mới   │  │  tokens còn    │         │   │
│  │  │   ↑ 23%        │  │   ↑ 1          │  │  [Nạp thêm]    │         │   │
│  │  └────────────────┘  └────────────────┘  └────────────────┘         │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **3. PROJECT CREATION - STEP 1: UPLOAD**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  [←]  Tạo dự án mới                              [1/3: Tải lên → 2: Thiết lập → 3: Viết] │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                                                                     │   │
│  │              ┌─────────────────────────────────────┐                │   │
│  │              │                                     │                │   │
│  │              │    [Icon: Cloud Upload]            │                │   │
│  │              │         Kéo thả file vào đây         │                │   │
│  │              │         hoặc click để chọn          │                │   │
│  │              │                                     │                │   │
│  │              │    Hỗ trợ: PDF, DOCX, TXT (Max 50MB)│                │   │
│  │              │                                     │                │   │
│  │              └─────────────────────────────────────┘                │   │
│  │                                                                     │   │
│  │  ────────────────  HOẶC  ────────────────                           │   │
│  │                                                                     │   │
│  │  [Bắt đầu từ template]  [Nhập thủ công]                            │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  📄 Y-ta-nho-cua-toi-outline.pdf          2.4 MB    [✓] Đã tải lên │   │
│  │                                                                     │   │
│  │  Đang phân tích cấu trúc...  ████████░░░░ 80%                      │   │
│  │                                                                     │   │
│  │  [Hủy]  [Phân tích lại]                                             │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  KẾT QUẢ PHÂN TÍCH                                                  │   │
│  │                                                                     │   │
│  │  Tiêu đề phát hiện:    "Y tá nhỏ của tôi"                           │   │
│  │  Thể loại:             Romance, Medical, Drama                      │   │
│  │  Độ dài ước tính:      20 chương, ~50,000 từ                        │   │
│  │                                                                     │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │  NHÂN VẬT PHÁT HIỆN                                        │   │   │
│  │  │                                                             │   │   │
│  │  │  • Trinh (20) - Nhân vật chính - Y tá mới ra trường        │   │   │
│  │  │  • An (35) - Bác sĩ phẫu thuật - Love interest             │   │   │
│  │  │                                                             │   │   │
│  │  │  [Xem chi tiết cấu trúc →]                                  │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  │                                                                     │   │
│  │  ⚠️ Cảnh báo nội dung: Phát hiện cảnh nhạy cảm trong outline       │   │
│  │     Hệ thống sẽ đề xuất chuyển đổi phù hợp khi viết               │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  [Lưu nháp]                                    [Tiếp tục: Chọn phong cách →] │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **4. PROJECT CREATION - STEP 2: CONFIGURATION**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  [←]  Thiết lập: Y tá nhỏ của tôi              [1 → 2/3: Thiết lập → 3: Viết] │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  CHỌN PHONG CÁCH VIẾT                                              │   │
│  │                                                                     │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │ ●  Romance Tinh khiết                                       │   │   │
│  │  │    Tập trung cảm xúc, nội tâm, kết thúc có hậu              │   │   │
│  │  │    Phù hợp: Ngôn tình sạch, truyện ngắn tình cảm            │   │   │
│  │  │    [✓ Đề xuất cho outline này]                              │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  │                                                                     │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │ ○  Drama                                                    │   │   │
│  │  │    Xung đột, phát triển nhân vật sâu sắc                    │   │   │
│  │  │    Phù hợp: Truyện chiều sâu, văn học                      │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  │                                                                     │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │ ○  Hài lãng mạn (Rom-Com)                                   │   │   │
│  │  │    Nhẹ nhàng, hài hước, tình huống vui nhộn                 │   │   │
│  │  │    Phù hợp: Giải trí, đọc để thư giãn                      │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  │                                                                     │   │
│  │  [Xem thêm: Thriller, Mystery, Slice of Life...]                   │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  TÙY CHỈNH NÂNG CAO                                                │   │
│  │                                                                     │   │
│  │  Độ dài mục tiêu:    [50,000] từ    [20] chương                    │   │
│  │                                                                     │   │
│  │  Ngôn ngữ:           [Tiếng Việt ▼]                                │   │
│  │                                                                     │   │
│  │  Góc nhìn:           [Ngôi thứ ba giới hạn ▼]                      │   │
│  │                      (theo nhân vật Trinh)                         │   │
│  │                                                                     │   │
│  │  Cấp độ miêu tả:     [Giàu chi tiết ▼]                             │   │
│  │                                                                     │   │
│  │  Tốc độ cốt truyện:  [Vừa phải ▼]                                  │   │
│  │                                                                     │   │
│  │  [⚙️ Mở rộng: Tone cảm xúc, Phong cách câu văn...]                 │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  XỬ LÝ NỘI DUNG NHẠY CẢM                                           │   │
│  │                                                                     │   │
│  │  Phát hiện: 3 cảnh cần chú ý trong outline                         │   │
│  │                                                                     │   │
│  │  [●] Tự động chuyển đổi sang phong cách đã chọn                     │   │
│  │  [○] Dừng lại để tôi review từng cảnh                               │   │
│  │  [○] Giữ nguyên (yêu cầu xác nhận)                                  │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  [← Quay lại]                                  [Bắt đầu viết →]            │
│  Dự kiến: 20 phút, ~5,000 tokens                                           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **5. WRITING PROGRESS SCREEN**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  [←]  Đang viết: Y tá nhỏ của tôi              [Live • WebSocket connected] │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  TIẾN ĐỘ TỔNG THỂ                                                  │   │
│  │                                                                     │   │
│  │  ██████████████████████░░░░░░░░░░  16/20 chương hoàn thành (80%)   │   │
│  │                                                                     │   │
│  │  Đang viết: Chapter 17 - "Tuần trăng mật trên máy bay"             │   │
│  │  Tiến độ chương: ████████████░░░░  ~1,800 / 2,200 từ               │   │
│  │                                                                     │   │
│  │  ⏱️ Còn khoảng 8 phút nữa                                          │   │
│  │                                                                     │   │
│  │  [Tạm dừng]  [Hủy]  [⚙️ Ưu tiên cao]                               │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  NHẬT KÝ VIẾT (Real-time)                                          │   │
│  │                                                                     │   │
│  │  14:32:05  ✓ Hoàn thành Chapter 16 - "Đám cưới" (2,450 từ)         │   │
│  │  14:32:18  ℹ️ Bắt đầu Chapter 17                                    │   │
│  │  14:33:45  ✓ Viết xong Scene 17.1 - Khởi đầu chuyến bay            │   │
│  │  14:35:12  ⚠️ Phát hiện cảnh nhạy cảm ở Scene 17.3                 │   │
│  │            → Tự động chuyển đổi: "Cảnh trong cabin"                │   │
│  │            → Đã viết lại: Tập trung vào cảm xúc, không gian        │   │
│  │  14:36:30  ✓ Tiếp tục Scene 17.4                                   │   │
│  │  14:38:15  ● Đang viết Scene 17.5 (hiện tại)                       │   │
│  │                                                                     │   │
│  │  [Xem chi tiết kỹ thuật]  [Tải log]                                 │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  XEM TRƯỚC CHƯƠNG GẦN NHẤT                                         │   │
│  │                                                                     │   │
│  │  ─── Chapter 16: Đám cưới ───                                      │   │
│  │                                                                     │   │
│  │  Sau tiệc cưới, An và Trinh về phòng tân hôn. Trinh còn nguyên     │   │
│  │  váy cưới trắng, An vest đen lịch lãm. Phòng ngập hoa hồng, nến    │   │
│  │  thơm lung linh.                                                   │   │
│  │                                                                     │   │
│  │  An bế Trinh qua ngưỡng cửa, đặt cô xuống giường. Ánh mắt anh       │   │
│  │  dịu dàng như chưa từng thấy trong suốt một năm qua...              │   │
│  │                                                                     │   │
│  │  [Đọc thêm →]  [Chỉnh sửa]                                          │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  CẢNH BÁO CẦN XEM XÉT                                              │   │
│  │                                                                     │   │
│  │  ⚠️ Chapter 12 - Scene 12.3                                        │   │
│  │     Nội dung: Mô tả thân mật trong phòng bệnh                      │   │
│  │     Hành động: Đã tự động viết lại theo phong cách Romance tinh    │   │
│  │                khiết                                               │   │
│  │     [Xem so sánh]  [Chấp nhận]  [Viết lại theo ý tôi]              │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **6. CHAPTER EDITOR**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  [←]  Y tá nhỏ của tôi  /  Chương 9: Thang máy và hầm xe                    │
│                                                                             │
│  [💾 Lưu] [↩️ Undo] [↪️ Redo] [🔄 Viết lại] [👁️ Xem trước] [⬇️ Xuất]     │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌────────────┐  ┌────────────────────────────────────────────────────────┐ │
│  │            │  │  ┌────────────────────────────────────────────────┐   │ │
│  │  CẤU TRÚC  │  │  │  CHAPTER 9: THANG MÁY VÀ HẦM XE                │   │ │
│  │            │  │  │                                                │   │ │
│  │  Scene 9.1 │  │  │  Chiều muộn, bệnh viện bắt đầu vắng bóng người. │   │ │
│  │  [✓]       │  │  │  Trinh đứng trước gương trong phòng thay đồ,   │   │ │
│  │            │  │  │  chỉnh lại chiếc váy hai dây màu xanh pastel   │   │ │
│  │  Scene 9.2 │  │  │  mà cô hiếm khi dám mặc...                      │   │ │
│  │  [✓]       │  │  │                                                │   │ │
│  │            │  │  │  ─────────────────────────────────────────────  │   │ │
│  │  Scene 9.3 │  │  │                                                │   │ │
│  │  [✓]       │  │  │  Hành lang vắng tanh. Trinh bước ra khỏi       │   │ │
│  │            │  │  │  phòng thay đồ, tay xách túi đựng đồng phục.   │   │ │
│  │  Scene 9.4 │  │  │  Tiếng giày cao gót vang lên trong không gian  │   │ │
│  │  [⚠️]      │  │  │  tĩnh mịch.                                     │   │ │
│  │  [Cần review]│ │  │                                                │   │ │
│  │            │  │  │  Cô đi về phía thang máy, định xuống hầm xe     │   │ │
│  │  Scene 9.5 │  │  │  thì một cánh cửa mở ra...                      │   │ │
│  │  [○]       │  │  │                                                │   │ │
│  │  (đang viết)│ │  │  An bước ra từ phòng thay đồ nam. Anh dừng      │   │ │
│  │            │  │  │  lại khi nhìn thấy Trinh.                       │   │ │
│  │            │  │  │                                                │   │ │
│  │            │  │  │  [Tiếp tục đọc...]                              │   │ │
│  │            │  │  │                                                │   │ │
│  │            │  │  │  ─────────────────────────────────────────────  │   │ │
│  │            │  │  │  2,340 từ  |  Đọc trong ~12 phút                │   │ │
│  │            │  │  └────────────────────────────────────────────────┘   │ │
│  │            │  │                                                     │ │
│  │            │  │  ┌────────────────────────────────────────────────┐   │ │
│  │            │  │  │  📝 GHI CHÚ CHO CHƯƠNG NÀY                     │   │ │
│  │            │  │  │                                                │   │ │
│  │            │  │  │  • Cảnh thang máy cần thêm căng thẳng hơn      │   │ │
│  │            │  │  │  • Đối thoại An hơi cứng, cần mềm mại hơn      │   │ │
│  │            │  │  │                                                │   │ │
│  │            │  │  │  [Thêm ghi chú]                                │   │ │
│  │            │  │  └────────────────────────────────────────────────┘   │ │
│  │            │  │                                                     │ │
│  └────────────┘  └────────────────────────────────────────────────────────┘ │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  THÔNG TIN CHƯƠNG                              AI: GPT-4 | Temp: 0.7 │   │
│  │                                                                     │   │
│  │  Trạng thái:  [✓ Hoàn thành]  [Sửa đổi lần cuối: 2 giờ trước]       │   │
│  │                                                                     │   │
│  │  Lịch sử phiên bản:                                                 │   │
│  │  [v1.0] [v1.1] [v1.2 ✓] [v1.3 (hiện tại)]                          │   │
│  │                                                                     │   │
│  │  [So sánh các phiên bản]  [Khôi phục v1.2]                          │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **7. SAFETY REVIEW MODAL**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  CẢNH CẦN XEM XÉT                                          [✕]            │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Chapter 9, Scene 9.4  |  Mức độ: Trung bình                                │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  LÝ DO PHÁT HIỆN                                                    │   │
│  │                                                                     │   │
│  │  AI phát hiện đoạn văn có thể chứa mô tả mang tính kích thích       │   │
│  │  không phù hợp với phong cách "Romance Tinh khiết" đã chọn.         │   │
│  │                                                                     │   │
│  │  Đoạn văn gốc:                                                      │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │ "Chiếc váy ôm sát lấy đường cong cơ thể, phô bày làn da      │   │   │
│  │  │  trắng ngần dưới ánh đèn hành lang..."                        │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  ĐỀ XUẤT CỦA AI (Đã tự động áp dụng)                                │   │
│  │                                                                     │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │ "Chiếc váy màu pastel khiến Trinh trông thật khác lạ,        │   │   │
│  │  │  như một nàng thơ bước ra từ tranh vẽ. An nhận ra mình       │   │   │
│  │  │  đã dừng lại để nhìn lâu hơn một nhịp thở..."                 │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  │                                                                     │   │
│  │  ✓ Giữ nguyên cảm xúc rung động                                    │   │
│  │  ✓ Chuyển focus từ cơ thể sang cảm nhận của nhân vật              │   │
│  │  ✗ Loại bỏ mô tả chi tiết hình thể                                 │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  LỰA CHỌN CỦA BẠN                                                  │   │
│  │                                                                     │   │
│  │  (●) Chấp nhận đề xuất của AI                                       │   │
│  │  (○) Tự viết lại                                                    │   │
│  │      ┌─────────────────────────────────────────────────────────┐   │   │
│  │      │ [Nhập văn bản của bạn...]                               │   │   │
│  │      └─────────────────────────────────────────────────────────┘   │   │
│  │  (○) Bỏ qua cảnh này (chapter sẽ ngắn hơn ~150 từ)                │   │
│  │  (○) Giữ nguyên bản gốc                                           │   │
│  │      ⚠️ Yêu cầu xác nhận: Nội dung có thể không phù hợp          │   │
│  │      [Tôi chịu trách nhiệm về nội dung này] ☐                    │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  [Áp dụng cho tất cả cảnh tương tự trong dự án]                            │
│                                                                             │
│                              [Hủy]  [Xác nhận lựa chọn →]                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **8. EXPORT SCREEN**

plain Copy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│  [←]  Xuất truyện: Y tá nhỏ của tôi                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  CHỌN ĐỊNH DẠNG                                                    │   │
│  │                                                                     │   │
│  │  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐         │   │
│  │  │  📕   │  │  📄   │  │  📝   │  │  📱   │  │  🎧   │         │   │
│  │  │  EPUB │  │  PDF  │  │  DOCX │  │  TXT  │  │  AUDIO│         │   │
│  │  │       │  │       │  │       │  │       │  │       │         │   │
│  │  │ [✓]   │  │ [○]   │  │ [○]   │  │ [○]   │  │ [○]   │         │   │
│  │  └────────┘  └────────┘  └────────┘  └────────┘  └────────┘         │   │
│  │                                                                     │   │
│  │  Đọc trên: Kindle, Kobo, Apple Books, Google Play Books            │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  TÙY CHỈNH EPUB                                                    │   │
│  │                                                                     │   │
│  │  [✓] Bao gồm bìa tự động tạo                                       │   │
│  │      [Tải lên bìa tùy chỉnh]  hoặc  [Chỉnh sửa bìa tự động →]      │   │
│  │                                                                     │   │
│  │  [✓] Bao gồm mục lục                                               │   │
│  │  [✓] Bao gồm thông tin tác giả                                     │   │
│  │  [○] Thêm watermark                                                │   │
│  │  [○] Bật bảo vệ DRM (Không cho phép sao chép)                      │   │
│  │                                                                     │   │
│  │  Font chữ:      [Noto Serif ▼]                                     │   │
│  │  Cỡ chữ:        [16 ▼]                                             │   │
│  │  Giãn dòng:     [1.6 ▼]                                            │   │
│  │                                                                     │   │
│  │  Cách chia chương:  [Trang mới cho mỗi chương ▼]                   │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  XEM TRƯỚC BÌA                                                     │   │
│  │                                                                     │   │
│  │  ┌─────────────────────────┐                                        │   │
│  │  │    ┌─────────────┐     │  Tiêu đề: Y tá nhỏ của tôi              │   │
│  │  │    │             │     │  Tác giả: [Tên của bạn]                │   │
│  │  │    │   [Cover    │     │  Thể loại: Romance, Medical            │   │
│  │  │    │    Image]   │     │  Độ dài: 45,230 từ | 20 chương         │   │
│  │  │    │             │     │                                        │   │
│  │  │    │   Y TÁ      │     │  [Chỉnh sửa thiết kế bìa →]            │   │
│  │  │    │   NHỎ       │     │                                        │   │
│  │  │    │   CỦA       │     │                                        │   │
│  │  │    │   TÔI       │     │                                        │   │
│  │  │    │             │     │                                        │   │
│  │  │    └─────────────┘     │                                        │   │
│  │  └─────────────────────────┘                                        │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  Dung lượng ước tính: ~3.5 MB                                               │
│                                                                             │
│  [Lưu preset]                          [Bắt đầu xuất →]  ⏱️ ~2 phút        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

#### **9. MOBILE RESPONSIVE**

plain Copy

```
┌─────────────────┐
│  [≡] StoryForge │
├─────────────────┤
│                 │
│  Xin chào! 👋   │
│                 │
│  ┌───────────┐  │
│  │  [➕]     │  │
│  │ Tạo mới   │  │
│  └───────────┘  │
│                 │
│  Đang viết:     │
│  ┌───────────┐  │
│  │ ████████░░│  │
│  │           │  │
│  │ Y tá nhỏ  │  │
│  │ 80%       │  │
│  │ [Tiếp tục]│  │
│  └───────────┘  │
│                 │
│  Gần đây:       │
│  ┌───┐ ┌───┐    │
│  │[C]│ │[C]│    │
│  │   │ │   │    │
│  │Hoàn│ │Hoàn│   │
│  │thành│ │thành│  │
│  └───┘ └───┘    │
│                 │
│  [🏠] [🔍] [💼] [👤]  │
└─────────────────┘

Tap project → 

┌─────────────────┐
│ [←] Y tá nhỏ... │
├─────────────────┤
│                 │
│  ████████░░ 80% │
│  Chương 16/20   │
│                 │
│  [Tiếp tục viết]│
│  [Xem chương]   │
│  [Chỉnh sửa]    │
│                 │
│  ─────────────  │
│                 │
│  Chương gần nhất│
│  Ch.16: Đám cưới│
│  "Sau tiệc cưới,│
│   An và Trinh..."│
│  [Đọc tiếp →]   │
│                 │
│  ─────────────  │
│                 │
│  Cảnh báo: 1    │
│  [Xem ngay →]   │
│                 │
└─────────────────┘
```

***

#### **10. COMPONENT LIBRARY**

**Buttons**

plain Copy

```
Primary:    [Lưu thay đổi]     Blue bg, white text, rounded-lg
Secondary:  [Hủy]             Gray border, gray text, transparent bg
Danger:     [Xóa dự án]       Red border, red text
Ghost:      [Chỉnh sửa →]     No border, blue text, hover underline
Icon:       [🗑️]              Icon only, circular, hover bg

States: default | hover | active | disabled | loading
```

**Form Elements**

plain Copy

```
Input:      ┌─────────────────┐  Rounded border, focus ring blue
            │ Nhập tiêu đề... │
            └─────────────────┘

Textarea:   ┌─────────────────┐  Auto-resize, min-height 100px
            │                 │
            │                 │
            └─────────────────┘

Select:     [Tiếng Việt ▼]    Custom dropdown, search enabled

Toggle:     [●────○] On/Off   Animated switch

Checkbox:   [✓] Bao gồm bìa   Custom checkmark, indeterminate state
```

**Cards**

plain Copy

```
Project Card:
┌─────────────────────────┐
│ [Cover Image 16:10]     │
│                         │
│ Title (truncate 2 lines)│
│ Genre • Status          │
│ Progress bar            │
│ [Actions ⋮]             │
└─────────────────────────┘

Chapter Card:
┌─────────────────────────┐
│ Ch.9: Title             │
│ ○ Pending / ● Completed │
│ 2,340 từ • 12 min read  │
│ [Edit] [Preview]        │
└─────────────────────────┘
```

**Feedback Components**

plain Copy

```
Toast:     ┌────────────────────────┐  Slide in from top-right
           │ ✓ Đã lưu thành công    │  Auto-dismiss 3s
           │              [✕]       │
           └────────────────────────┘

Modal:     ┌────────────────────────┐  Centered, backdrop blur
           │ Title              [✕] │
           ├────────────────────────┤
           │ Content...             │
           │                        │
           │    [Hủy]  [Xác nhận]   │
           └────────────────────────┘

Skeleton:  ┌────────────────────────┐  Animated pulse loading
           │ ████████████████████   │
           │ ██████████             │
           │ █████████████████████  │
           └────────────────────────┘
```

***

Đây là bản spec đầy đủ cho ứng dụng StoryForge. Bạn cần tôi bổ sung thêm phần nào không?


# SPECIFICATION: ỨNG DỤNG THEO DÕI BIẾN ĐỘNG GIÁ CHỨNG KHOÁN MBS

### 1. TỔNG QUAN DỰ ÁN

#### 1.1 Mục tiêu

Xây dựng ứng dụng web/mobile theo dõi biến động giá chứng khoán theo thời gian thực từ sàn chứng khoán MB Securities (MBS), cung cấp công cụ phân tích và cảnh báo cho nhà đầu tư.

#### 1.2 Phạm vi

* **Nguồn dữ liệu**: Sàn chứng khoán MBS (mbs.com.vn) + các nguồn dữ liệu thị trường VN (VnDirect, SSI, Vietstock)
* **Thị trường**: HOSE, HNX, UPCOM
* **Loại chứng khoán**: Cổ phiếu, ETF, Chứng quyền, Trái phiếu doanh nghiệp

***

### 2. KIẾN TRÚC HỆ THỐNG

plain Copy

```
┌─────────────────────────────────────────────────────────────┐
│                    CLIENT LAYER                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Web App    │  │  Mobile App  │  │  Extension   │      │
│  │  (React/Vue) │  │(React Native)│  │   (Chrome)   │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└────────────────────┬────────────────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────────────────┐
│                   API GATEWAY                               │
│         (Rate Limiting, Authentication, Routing)            │
└────────────────────┬────────────────────────────────────────┘
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
┌──────────────┐ ┌──────────┐ ┌──────────────┐
│  Data Sync   │ │  Realtime│ │  Historical  │
│   Service    │ │  Service │ │   Service    │
└──────┬───────┘ └────┬─────┘ └──────┬───────┘
       │              │              │
       └──────────────┼──────────────┘
                      │
┌─────────────────────▼───────────────────────────────────────┐
│              DATA SOURCES                                   │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
│  │ MBS WebSocket│ │  VnStock API │ │  SSI FC API  │        │
│  │  (Primary)   │ │  (Fallback)  │ │  (Backup)    │        │
│  └──────────────┘ └──────────────┘ └──────────────┘        │
└─────────────────────────────────────────────────────────────┘
```

***

### 3. CHỨC NĂNG CHÍNH (FUNCTIONAL REQUIREMENTS)

#### 3.1 Dashboard Tổng Quan Thị Trường

Table

| ID    | Chức năng              | Mô tả                                                     | Priority |
| ----- | ---------------------- | --------------------------------------------------------- | -------- |
| F-001 | Bảng giá trực tuyến    | Hiển thị bảng giá HOSE/HNX/UPCOM với tốc độ cập nhật < 1s | P0       |
| F-002 | Chỉ số thị trường      | VN-Index, HNX-Index, VN30, VNMidCap, VNSmallCap           | P0       |
| F-003 | Top movers             | Top tăng/giảm/giao dịch nhiều nhất                        | P0       |
| F-004 | Heatmap thị trường     | Màu sắc thể hiện độ rộng thị trường                       | P1       |
| F-005 | Thanh khoản thị trường | Tổng khối lượng, giá trị giao dịch                        | P1       |

#### 3.2 Theo Dõi Cổ Phiếu Chi Tiết

Table

| ID    | Chức năng                      | Mô tả                                             | Priority |
| ----- | ------------------------------ | ------------------------------------------------- | -------- |
| F-006 | Biểu đồ nến (Candlestick)      | Khung thời gian: 1m, 5m, 15m, 30m, 1H, 1D, 1W, 1M | P0       |
| F-007 | Bảng giá đặt lệnh (Order Book) | Hiển thị 10 mức giá mua/bán tốt nhất              | P0       |
| F-008 | Lịch sử giao dịch (Tape)       | Danh sách giao dịch khớp lệnh real-time           | P0       |
| F-009 | Thông tin cơ bản               | Vốn hóa, EPS, P/E, P/B, ROE, ROA                  | P1       |
| F-010 | So sánh cổ phiếu               | So sánh 2-5 mã trên cùng biểu đồ                  | P1       |

#### 3.3 Hệ Thống Cảnh Báo (Alert System)

Table

| ID    | Chức năng            | Mô tả                                            | Priority |
| ----- | -------------------- | ------------------------------------------------ | -------- |
| F-011 | Cảnh báo giá         | Khi giá chạm ngưỡng (>, <, =) giá định trước     | P0       |
| F-012 | Cảnh báo biến động % | Khi thay đổi % vượt ngưỡng trong khung thời gian | P0       |
| F-013 | Cảnh báo khối lượng  | Khi khối lượng giao dịch vượt ngưỡng             | P1       |
| F-014 | Cảnh báo breakout    | Khi phá vỡ đỉnh/đáy trong N phiên                | P2       |
| F-015 | Thông báo đa kênh    | Web push, Email, Telegram, SMS                   | P1       |

#### 3.4 Quản Lý Danh Mục Đầu Tư

Table

| ID    | Chức năng          | Mô tả                                     | Priority |
| ----- | ------------------ | ----------------------------------------- | -------- |
| F-016 | Tạo danh mục ảo    | Theo dõi nhiều danh mục với tên tùy chỉnh | P1       |
| F-017 | Nhập giao dịch     | Ghi nhận mua/bán với giá, số lượng, ngày  | P1       |
| F-018 | Tính P\&L          | Lãi/lỗ thực tế và unrealized P\&L         | P1       |
| F-019 | Phân tích danh mục | Tỷ trọng ngành, biểu đồ phân bổ           | P2       |
| F-020 | Export báo cáo     | PDF/Excel báo cáo danh mục                | P2       |

***

### 4. YÊU CẦU KỸ THUẬT (TECHNICAL REQUIREMENTS)

#### 4.1 Data Sources & APIs

**4.1.1 Nguồn chính: MBS WebSocket**

JavaScript Copy

```javascript
// WebSocket Endpoint (giả định dựa trên cấu trúc s24.mbs.com.vn)
wss://realtime.mbs.com.vn/ws/market-data

// Subscription Message
{
  "action": "subscribe",
  "channels": ["price", "orderbook", "trade"],
  "symbols": ["VNM", "VIC", "HPG"],
  "frequency": "realtime"
}

// Response Format
{
  "type": "price_update",
  "symbol": "VNM",
  "data": {
    "lastPrice": 78500,
    "change": 1200,
    "changePercent": 1.55,
    "volume": 1250000,
    "bid": 78400,
    "ask": 78600,
    "timestamp": "2024-01-15T09:45:23.123Z"
  }
}
```

**4.1.2 Nguồn dự phòng: VnStock API**

Python Copy

```python
# Sử dụng thư viện vnstock cho dữ liệu lịch sử
from vnstock import Vnstock

stock = Vnstock().stock(symbol='VNM', source='VCI')
df = stock.quote.history(start='2024-01-01', end='2024-01-15')
```

**4.1.3 Nguồn dự phòng 2: SSI FC API**

* **Base URL**: `https://fc-data.ssi.com.vn/`
* **Streaming**: WebSocket cho dữ liệu real-time
* **Documentation**: Yêu cầu tài khoản SSI để truy cập

#### 4.2 Công Nghệ Sử Dụng

Table

| Layer            | Công nghệ                        | Lý do chọn                    |
| ---------------- | -------------------------------- | ----------------------------- |
| Frontend         | Next.js 14 + TypeScript          | SSR, performance, type safety |
| State Management | Zustand + React Query            | Lightweight, caching tốt      |
| Charts           | Lightweight Charts (TradingView) | Chuyên dụng tài chính, nhẹ    |
| Backend          | Node.js + Fastify                | Hiệu năng cao, WebSocket tốt  |
| Database         | TimescaleDB (PostgreSQL)         | Time-series data, query nhanh |
| Cache            | Redis                            | Real-time data, session       |
| Message Queue    | Redis Pub/Sub                    | Phân phối dữ liệu real-time   |
| Hosting          | Vercel (FE) + AWS EC2 (BE)       | Scalable, cost-effective      |

#### 4.3 Database Schema

sql Copy

```sql
-- Bảng giá real-time
CREATE TABLE stock_prices (
    symbol VARCHAR(10) NOT NULL,
    timestamp TIMESTAMPTZ NOT NULL,
    open DECIMAL(12,2),
    high DECIMAL(12,2),
    low DECIMAL(12,2),
    close DECIMAL(12,2),
    volume BIGINT,
    value BIGINT,
    bid_price DECIMAL(12,2)[],
    bid_volume BIGINT[],
    ask_price DECIMAL(12,2)[],
    ask_volume BIGINT[],
    PRIMARY KEY (symbol, timestamp)
);

-- Bảng giao dịch (tape)
CREATE TABLE trades (
    id BIGSERIAL,
    symbol VARCHAR(10) NOT NULL,
    timestamp TIMESTAMPTZ NOT NULL,
    price DECIMAL(12,2),
    volume BIGINT,
    side CHAR(1), -- 'B'uy or 'S'ell
    match_type VARCHAR(10) -- 'LO', 'MP', 'ATC', etc.
);

-- Bảng cảnh báo
CREATE TABLE alerts (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    symbol VARCHAR(10) NOT NULL,
    condition_type VARCHAR(20), -- 'PRICE_ABOVE', 'PRICE_BELOW', 'CHANGE_PERCENT'
    threshold DECIMAL(12,4),
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    triggered_at TIMESTAMPTZ,
    notification_channels JSONB -- ['web', 'email', 'telegram']
);

-- Index cho query nhanh
CREATE INDEX idx_prices_symbol_time ON stock_prices(symbol, timestamp DESC);
CREATE INDEX idx_trades_symbol_time ON trades(symbol, timestamp DESC);
```

***

### 5. GIAO DIỆN NGƯỜI DÙNG (UI/UX)

#### 5.1 Wireframe Chính

plain Copy

```
┌─────────────────────────────────────────────────────────────────┐
│  [Logo]  Dashboard  Watchlist  Screener  Alerts  [🔍] [👤] [⚙️] │
├─────────────────────────────────────────────────────────────────┤
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  VN-Index: 1,245.67 ▲12.5 (+1.02%)  |  KLGD: 890.5 tỷ    │  │
│  └───────────────────────────────────────────────────────────┘  │
│  ┌──────────────────────┐  ┌─────────────────────────────────┐  │
│  │   [Biểu đồ nến       │  │  Bảng giá trực tuyến           │  │
│  │    VNM - 1 ngày]     │  │  ─────────────────────────────  │  │
│  │                      │  │  Mã    Giá   +/-   KL    Tổng KL│  │
│  │  [Toolbar chart]     │  │  VNM  78.5  ▲1.2  125K   1.2M   │  │
│  │                      │  │  VIC  95.2  ▼0.5   89K   890K   │  │
│  │                      │  │  HPG  24.8  ▲0.3  456K   5.6M   │  │
│  └──────────────────────┘  │  ...                            │  │
│  ┌──────────────────────┐  └─────────────────────────────────┘  │
│  │  Order Book (VNM)    │  ┌─────────────────────────────────┐  │
│  │  Giá mua    KL       │  │  Top Movers                     │  │
│  │  78.6      15,400    │  │  🚀 Tăng mạnh: ACB +5.2%        │  │
│  │  78.5      28,500    │  │  📉 Giảm mạnh: GAS -3.1%        │  │
│  │  ...                 │  │  🔥 Khớp nhiều: HPG (56 tỷ)     │  │
│  │  ─────────────────   │  └─────────────────────────────────┘  │
│  │  Giá bán    KL       │                                       │
│  │  78.7      12,300    │                                       │
│  │  ...                 │                                       │
│  └──────────────────────┘                                       │
└─────────────────────────────────────────────────────────────────┘
```

#### 5.2 Responsive Breakpoints

* **Desktop**: 1440px+ (multi-panel layout)
* **Tablet**: 768px-1439px (2-column layout)
* **Mobile**: <768px (single column, tab navigation)

***

### 6. HIỆU NĂNG & BẢO MẬT

#### 6.1 Yêu Cầu Hiện Năng

Table

| Chỉ số             | Yêu cầu                 | Cách đo                    |
| ------------------ | ----------------------- | -------------------------- |
| Latency dữ liệu    | < 500ms từ sàn đến user | WebSocket timestamp diff   |
| Time to First Byte | < 100ms                 | Lighthouse/Performance API |
| Frame rate         | 60fps khi scroll/chart  | Chrome DevTools            |
| Concurrent users   | 10,000+                 | Load testing (k6)          |

#### 6.2 Bảo Mật

* **Authentication**: JWT với refresh token rotation
* **Rate Limiting**: 100 requests/minute cho API, 10 connections/IP cho WebSocket
* **Data Validation**: Zod schema validation cho mọi input
* **CORS**: Whitelist domain cụ thể
* **HTTPS/WSS**: Bắt buộc cho mọi connection

***

### 7. LỘ TRÌNH PHÁT TRIỂN (ROADMAP)

#### Phase 1: MVP (4 tuần)

* \[ ] Kết nối WebSocket MBS lấy dữ liệu real-time
* \[ ] Dashboard cơ bản (bảng giá, biểu đồ đơn giản)
* \[ ] Danh sách theo dõi (watchlist) local storage

#### Phase 2: Core Features (4 tuần)

* \[ ] Hệ thống cảnh báo giá
* \[ ] Biểu đồ nến nâng cao (indicators cơ bản)
* \[ ] User authentication & cloud watchlist

#### Phase 3: Advanced (4 tuần)

* \[ ] Quản lý danh mục đầu tư với P\&L
* \[ ] Phân tích kỹ thuật (RSI, MACD, MA)
* \[ ] Mobile app (React Native)

#### Phase 4: Scale (Ongoing)

* \[ ] Machine learning dự đoán xu hướng
* \[ ] Tích hợp tin tức & sentiment analysis
* \[ ] Social features (chia sẻ danh mục)

***

### 8. RỦI RO & GIẢI PHÁP

Table

| Rủi ro                  | Mức độ     | Giải pháp                                                         |
| ----------------------- | ---------- | ----------------------------------------------------------------- |
| MBS không có public API | Cao        | Web scraping + reverse engineering WebSocket, có fallback sources |
| Delay dữ liệu           | Trung bình | Multi-source aggregation, hiển thị timestamp và độ trễ            |
| Rate limiting từ nguồn  | Trung bình | Cache aggressive, exponential backoff                             |
| Dữ liệu không chính xác | Cao        | Cross-validation giữa nhiều nguồn, disclaimer rõ ràng             |

***

### 9. TÀI LIỆU THAM KHẢO

* API Stock VN - apistockvn.robotstock.info.vn
* SSI FC API Documentation - fc-data.ssi.com.vn
* VnStock Python Library - Thinh Vu

***

Bạn có muốn tôi chi tiết hóa thêm phần nào không? Ví dụ:

* **API Integration** chi tiết với code mẫu
* **Database Schema** đầy đủ với migration scripts
* **UI/UX Design** với Figma wireframes chi tiết
* **DevOps** với Docker, CI/CD pipeline


# API Integration và Database Schema

## 🔌 PHẦN 1: API INTEGRATION CHI TIẾT

### 1.1 Kiến Trúc Tổng Quan

plain Copy

```
┌─────────────────────────────────────────────────────────────┐
│                    CLIENT (Next.js)                         │
│         ┌─────────────────┐    ┌─────────────────┐         │
│         │  React Query    │◄──►│  Zustand Store  │         │
│         │  (Caching)      │    │  (State Mgmt)   │         │
│         └────────┬────────┘    └─────────────────┘         │
│                  │                                          │
│         ┌────────▼────────┐                                │
│         │  API Client     │                                │
│         │  (Axios/Fetch)  │                                │
│         └────────┬────────┘                                │
└──────────────────┼──────────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────────┐
│                 API ROUTES (Next.js API)                    │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │ /api/stocks │ │/api/realtime│ │ /api/alerts │           │
│  │   (REST)    │ │ (WebSocket) │ │   (REST)    │           │
│  └──────┬──────┘ └──────┬──────┘ └──────┬──────┘           │
└─────────┼───────────────┼───────────────┼───────────────────┘
          │               │               │
    ┌─────┴─────┐   ┌─────┴─────┐   ┌─────┴─────┐
    │ MBS API   │   │ Redis     │   │ PostgreSQL│
    │ (Primary) │   │ (Cache)   │   │ (Storage) │
    └───────────┘   └───────────┘   └───────────┘
```

### 1.2 MBS WebSocket Integration

#### WebSocket Client Service

TypeScript Copy

```typescript
// services/mbs-websocket.ts
import { EventEmitter } from 'events';
import { createHash } from 'crypto';

interface MBSConfig {
  wsUrl: string;
  apiKey?: string;
  reconnectInterval?: number;
  maxReconnectAttempts?: number;
}

interface StockData {
  symbol: string;
  lastPrice: number;
  change: number;
  changePercent: number;
  volume: number;
  value: number;
  bidPrice: number[];
  bidVolume: number[];
  askPrice: number[];
  askVolume: number[];
  high: number;
  low: number;
  open: number;
  reference: number;
  timestamp: string;
}

class MBSWebSocketClient extends EventEmitter {
  private ws: WebSocket | null = null;
  private config: MBSConfig;
  private reconnectAttempts = 0;
  private subscribedSymbols: Set<string> = new Set();
  private isConnected = false;
  private heartbeatInterval: NodeJS.Timeout | null = null;

  constructor(config: MBSConfig) {
    super();
    this.config = {
      reconnectInterval: 5000,
      maxReconnectAttempts: 10,
      ...config
    };
  }

  connect(): void {
    try {
      // MBS WebSocket URL structure (giả định dựa trên phân tích s24.mbs.com.vn)
      const wsUrl = `${this.config.wsUrl}?token=${this.generateToken()}`;
      
      this.ws = new WebSocket(wsUrl);
      
      this.ws.onopen = this.handleOpen.bind(this);
      this.ws.onmessage = this.handleMessage.bind(this);
      this.ws.onclose = this.handleClose.bind(this);
      this.ws.onerror = this.handleError.bind(this);
      
    } catch (error) {
      console.error('WebSocket connection error:', error);
      this.scheduleReconnect();
    }
  }

  private generateToken(): string {
    // Giả lập token generation - cần reverse engineering từ MBS web app
    const timestamp = Date.now();
    const secret = this.config.apiKey || 'mbs-public-key';
    return createHash('sha256')
      .update(`${secret}${timestamp}`)
      .digest('hex')
      .substring(0, 32);
  }

  private handleOpen(): void {
    console.log('MBS WebSocket connected');
    this.isConnected = true;
    this.reconnectAttempts = 0;
    
    // Resubscribe to previous symbols
    this.subscribedSymbols.forEach(symbol => this.subscribe(symbol));
    
    // Start heartbeat
    this.startHeartbeat();
    
    this.emit('connected');
  }

  private handleMessage(event: MessageEvent): void {
    try {
      const data = JSON.parse(event.data);
      
      // Xử lý các loại message khác nhau từ MBS
      switch (data.type) {
        case 'stock_update':
          this.handleStockUpdate(data.payload);
          break;
        case 'market_status':
          this.emit('marketStatus', data.payload);
          break;
        case 'trade_notification':
          this.emit('trade', data.payload);
          break;
        case 'heartbeat':
          // Reset heartbeat timeout
          break;
        default:
          console.log('Unknown message type:', data.type);
      }
    } catch (error) {
      console.error('Error parsing message:', error);
    }
  }

  private handleStockUpdate(payload: any): void {
    const stockData: StockData = {
      symbol: payload.sym,
      lastPrice: parseFloat(payload.last),
      change: parseFloat(payload.change),
      changePercent: parseFloat(payload.changePct),
      volume: parseInt(payload.vol),
      value: parseInt(payload.val),
      bidPrice: payload.bidP || [],
      bidVolume: payload.bidV || [],
      askPrice: payload.askP || [],
      askVolume: payload.askV || [],
      high: parseFloat(payload.high),
      low: parseFloat(payload.low),
      open: parseFloat(payload.open),
      reference: parseFloat(payload.ref),
      timestamp: payload.time
    };

    // Cache to Redis
    this.cacheToRedis(stockData);
    
    // Emit to subscribers
    this.emit('stockUpdate', stockData);
  }

  private async cacheToRedis(data: StockData): Promise<void> {
    // Sử dụng Redis để cache real-time data
    const redis = await getRedisClient();
    await redis.setEx(
      `stock:${data.symbol}:latest`,
      60, // 60 seconds TTL
      JSON.stringify(data)
    );
    
    // Publish to Redis channel cho các service khác
    await redis.publish('stock:updates', JSON.stringify(data));
  }

  private handleClose(event: CloseEvent): void {
    console.log('WebSocket closed:', event.code, event.reason);
    this.isConnected = false;
    this.stopHeartbeat();
    
    if (event.code !== 1000) { // Không phải close chủ động
      this.scheduleReconnect();
    }
  }

  private handleError(error: Event): void {
    console.error('WebSocket error:', error);
    this.emit('error', error);
  }

  private scheduleReconnect(): void {
    if (this.reconnectAttempts >= this.config.maxReconnectAttempts!) {
      console.error('Max reconnect attempts reached');
      this.emit('maxReconnectAttemptsReached');
      return;
    }

    this.reconnectAttempts++;
    console.log(`Reconnecting in ${this.config.reconnectInterval}ms... (Attempt ${this.reconnectAttempts})`);
    
    setTimeout(() => {
      this.connect();
    }, this.config.reconnectInterval);
  }

  subscribe(symbol: string): void {
    if (!this.isConnected || !this.ws) {
      this.subscribedSymbols.add(symbol);
      return;
    }

    const message = {
      action: 'subscribe',
      channel: 'stock',
      symbol: symbol.toUpperCase()
    };

    this.ws.send(JSON.stringify(message));
    this.subscribedSymbols.add(symbol);
    console.log(`Subscribed to ${symbol}`);
  }

  unsubscribe(symbol: string): void {
    if (!this.isConnected || !this.ws) return;

    const message = {
      action: 'unsubscribe',
      channel: 'stock',
      symbol: symbol.toUpperCase()
    };

    this.ws.send(JSON.stringify(message));
    this.subscribedSymbols.delete(symbol);
  }

  subscribeMarketIndices(): void {
    const indices = ['VNINDEX', 'HNXINDEX', 'UPCOMINDEX', 'VN30'];
    indices.forEach(index => {
      const message = {
        action: 'subscribe',
        channel: 'index',
        symbol: index
      };
      this.ws?.send(JSON.stringify(message));
    });
  }

  private startHeartbeat(): void {
    this.heartbeatInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
      }
    }, 30000); // 30 seconds
  }

  private stopHeartbeat(): void {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }

  disconnect(): void {
    this.stopHeartbeat();
    if (this.ws) {
      this.ws.close(1000, 'Client disconnect');
      this.ws = null;
    }
    this.isConnected = false;
  }
}

// Singleton instance
let mbsClient: MBSWebSocketClient | null = null;

export function getMBSClient(config?: MBSConfig): MBSWebSocketClient {
  if (!mbsClient && config) {
    mbsClient = new MBSWebSocketClient(config);
  }
  return mbsClient!;
}

export default MBSWebSocketClient;
```

#### WebSocket Server (Next.js Route Handler)

TypeScript Copy

```typescript
// app/api/realtime/route.ts
import { NextRequest } from 'next/server';
import { getMBSClient } from '@/services/mbs-websocket';

export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
  const upgrade = request.headers.get('upgrade');
  
  if (upgrade !== 'websocket') {
    return new Response('Expected websocket', { status: 400 });
  }

  // Sử dụng Node.js runtime cho WebSocket
  const { socket, response } = await upgradeWebSocket(request);
  
  const client = getMBSClient({
    wsUrl: process.env.MBS_WS_URL || 'wss://realtime.mbs.com.vn/ws',
    apiKey: process.env.MBS_API_KEY
  });

  // Kết nối tới MBS nếu chưa connect
  if (!client.listenerCount('stockUpdate')) {
    client.connect();
  }

  // Gửi data tới client khi có update từ MBS
  const handleUpdate = (data: any) => {
    socket.send(JSON.stringify(data));
  };

  client.on('stockUpdate', handleUpdate);
  client.on('marketStatus', handleUpdate);

  // Xử lý message từ client (subscribe/unsubscribe)
  socket.on('message', (message: string) => {
    try {
      const { action, symbol } = JSON.parse(message);
      
      if (action === 'subscribe') {
        client.subscribe(symbol);
      } else if (action === 'unsubscribe') {
        client.unsubscribe(symbol);
      }
    } catch (error) {
      console.error('Invalid message from client:', error);
    }
  });

  // Cleanup khi client disconnect
  socket.on('close', () => {
    client.off('stockUpdate', handleUpdate);
    client.off('marketStatus', handleUpdate);
  });

  return response;
}

// Helper function để upgrade HTTP thành WebSocket
async function upgradeWebSocket(request: NextRequest) {
  // Implementation tùy thuộc vào runtime (Node.js/Edge)
  // Sử dụng thư viện như 'ws' cho Node.js runtime
  const { default: WebSocket } = await import('ws');
  
  // ... implementation details
}
```

### 1.3 Fallback APIs (VnStock & SSI)

#### VnStock Integration Service

TypeScript Copy

```typescript
// services/vnstock-api.ts
import axios, { AxiosInstance } from 'axios';

interface HistoricalData {
  date: string;
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number;
  value: number;
}

interface CompanyProfile {
  symbol: string;
  companyName: string;
  industry: string;
  sector: string;
  marketCap: number;
  sharesOutstanding: number;
  eps: number;
  pe: number;
  pb: number;
  roe: number;
  roa: number;
}

class VnStockAPI {
  private client: AxiosInstance;
  private baseURL = 'https://apipubaws.tcbs.com.vn';

  constructor() {
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 10000,
      headers: {
        'Accept': 'application/json',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
      }
    });

    // Response interceptor để xử lý rate limiting
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        if (error.response?.status === 429) {
          // Rate limited - đợi 1s rồi retry
          await new Promise(resolve => setTimeout(resolve, 1000));
          return this.client.request(error.config);
        }
        return Promise.reject(error);
      }
    );
  }

  // Lấy dữ liệu lịch sử giá
  async getHistoricalData(
    symbol: string, 
    startDate: string, 
    endDate: string,
    resolution: '1' | '5' | '15' | '30' | '60' | '1D' | '1W' | '1M' = '1D'
  ): Promise<HistoricalData[]> {
    try {
      const response = await this.client.get('/stock-insight/v1/stock/bars-long-term', {
        params: {
          ticker: symbol.toUpperCase(),
          type: resolution,
          from: startDate,
          to: endDate
        }
      });

      return response.data.data.map((item: any) => ({
        date: item.tradingDate,
        open: item.open,
        high: item.high,
        low: item.low,
        close: item.close,
        volume: item.volume,
        value: item.value
      }));
    } catch (error) {
      console.error(`Error fetching historical data for ${symbol}:`, error);
      throw error;
    }
  }

  // Lấy thông tin công ty
  async getCompanyProfile(symbol: string): Promise<CompanyProfile> {
    try {
      const [profileRes, financialRes] = await Promise.all([
        this.client.get('/tcanalysis/v1/ticker/${symbol}/overview'),
        this.client.get('/tcanalysis/v1/finance/${symbol}/financialratio')
      ]);

      const profile = profileRes.data;
      const financial = financialRes.data;

      return {
        symbol: symbol.toUpperCase(),
        companyName: profile.companyName,
        industry: profile.industry,
        sector: profile.sector,
        marketCap: profile.marketCap,
        sharesOutstanding: profile.sharesOutstanding,
        eps: financial.eps,
        pe: financial.pe,
        pb: financial.pb,
        roe: financial.roe,
        roa: financial.roa
      };
    } catch (error) {
      console.error(`Error fetching company profile for ${symbol}:`, error);
      throw error;
    }
  }

  // Lấy danh sách cổ phiếu
  async getStockList(exchange: 'HOSE' | 'HNX' | 'UPCOM' | 'ALL' = 'ALL'): Promise<string[]> {
    try {
      const response = await this.client.get('/stock-insight/v1/stock/stock-recommendation', {
        params: { exchange }
      });
      
      return response.data.data.map((item: any) => item.ticker);
    } catch (error) {
      console.error('Error fetching stock list:', error);
      throw error;
    }
  }

  // Lấy dữ liệu intraday (1 phút) cho ngày hiện tại
  async getIntradayData(symbol: string): Promise<HistoricalData[]> {
    const today = new Date().toISOString().split('T')[0];
    
    try {
      const response = await this.client.get('/stock-insight/v1/stock/bars', {
        params: {
          ticker: symbol.toUpperCase(),
          type: '1',
          from: today,
          to: today
        }
      });

      return response.data.data.map((item: any) => ({
        date: item.tradingDate,
        open: item.open,
        high: item.high,
        low: item.low,
        close: item.close,
        volume: item.volume,
        value: item.value
      }));
    } catch (error) {
      console.error(`Error fetching intraday data for ${symbol}:`, error);
      throw error;
    }
  }
}

export const vnstockAPI = new VnStockAPI();
export default VnStockAPI;
```

#### SSI FastConnect API Integration

TypeScript Copy

```typescript
// services/ssi-api.ts
import axios, { AxiosInstance } from 'axios';
import crypto from 'crypto';

interface SSIToken {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
  obtainedAt: number;
}

class SSIFastConnectAPI {
  private client: AxiosInstance;
  private consumerID: string;
  private consumerSecret: string;
  private token: SSIToken | null = null;
  private baseURL = 'https://fc-data.ssi.com.vn';

  constructor(consumerID: string, consumerSecret: string) {
    this.consumerID = consumerID;
    this.consumerSecret = consumerSecret;
    
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 15000,
      headers: {
        'Content-Type': 'application/json'
      }
    });

    // Request interceptor để tự động thêm token
    this.client.interceptors.request.use(async (config) => {
      if (!this.token || this.isTokenExpired()) {
        await this.authenticate();
      }
      
      config.headers.Authorization = `Bearer ${this.token?.accessToken}`;
      return config;
    });

    // Response interceptor để handle token expiration
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;
        
        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          await this.authenticate();
          originalRequest.headers.Authorization = `Bearer ${this.token?.accessToken}`;
          return this.client(originalRequest);
        }
        
        return Promise.reject(error);
      }
    );
  }

  private async authenticate(): Promise<void> {
    try {
      const response = await axios.post(`${this.baseURL}/api/v2/Market/AccessToken`, {
        consumerID: this.consumerID,
        consumerSecret: this.consumerSecret
      });

      this.token = {
        accessToken: response.data.data.accessToken,
        refreshToken: response.data.data.refreshToken,
        expiresIn: response.data.data.expiresIn,
        obtainedAt: Date.now()
      };
    } catch (error) {
      console.error('SSI Authentication failed:', error);
      throw error;
    }
  }

  private isTokenExpired(): boolean {
    if (!this.token) return true;
    const expiresAt = this.token.obtainedAt + (this.token.expiresIn * 1000);
    return Date.now() >= expiresAt - 60000; // Refresh 1 phút trước khi hết hạn
  }

  // Lấy danh mục chứng khoán
  async getSecurities(exchange: string = 'HOSE'): Promise<any[]> {
    const response = await this.client.get('/api/v2/Market/Securities', {
      params: { exchange }
    });
    return response.data.data;
  }

  // Lấy chỉ số thị trường
  async getIndices(): Promise<any[]> {
    const response = await this.client.get('/api/v2/Market/Indices');
    return response.data.data;
  }

  // Lấy báo giá (quote)
  async getQuotes(symbols: string[]): Promise<any[]> {
    const response = await this.client.get('/api/v2/Market/Quotes', {
      params: {
        symbols: symbols.join(',')
      }
    });
    return response.data.data;
  }

  // Lấy dữ liệu lịch sử
  async getDailyOhlc(
    symbol: string, 
    fromDate: string, 
    toDate: string,
    pageIndex: number = 1,
    pageSize: number = 100
  ): Promise<any> {
    const response = await this.client.get('/api/v2/Market/DailyOhlc', {
      params: {
        symbol,
        fromDate,
        toDate,
        pageIndex,
        pageSize,
        ascending: true
      }
    });
    return response.data;
  }

  // Lấy dữ liệu giao dịch intraday
  async getIntradayOhlc(
    symbol: string,
    fromDate: string,
    toDate: string,
    resolution: '1' | '5' | '15' | '30' | '60' = '1',
    pageIndex: number = 1,
    pageSize: number = 100
  ): Promise<any> {
    const response = await this.client.get('/api/v2/Market/IntradayOhlc', {
      params: {
        symbol,
        fromDate,
        toDate,
        resolution,
        pageIndex,
        pageSize,
        ascending: true
      }
    });
    return response.data;
  }
}

export default SSIFastConnectAPI;
```

### 1.4 API Routes (Next.js App Router)

#### Stock Data API

TypeScript Copy

```typescript
// app/api/stocks/[symbol]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { vnstockAPI } from '@/services/vnstock-api';
import { getRedisClient } from '@/lib/redis';
import { prisma } from '@/lib/prisma';

export async function GET(
  request: NextRequest,
  { params }: { params: { symbol: string } }
) {
  const { symbol } = params;
  const searchParams = request.nextUrl.searchParams;
  const type = searchParams.get('type') || 'overview'; // overview, historical, intraday, profile

  try {
    const cacheKey = `stock:${symbol.toUpperCase()}:${type}`;
    const redis = await getRedisClient();

    // Try cache first
    const cached = await redis.get(cacheKey);
    if (cached) {
      return NextResponse.json(JSON.parse(cached), {
        headers: {
          'X-Cache': 'HIT',
          'X-Cache-TTL': '60'
        }
      });
    }

    let data;

    switch (type) {
      case 'overview':
        data = await getStockOverview(symbol);
        break;
      case 'historical':
        const from = searchParams.get('from') || getDefaultFromDate();
        const to = searchParams.get('to') || getToday();
        const resolution = searchParams.get('resolution') as any || '1D';
        data = await vnstockAPI.getHistoricalData(symbol, from, to, resolution);
        break;
      case 'intraday':
        data = await vnstockAPI.getIntradayData(symbol);
        break;
      case 'profile':
        data = await vnstockAPI.getCompanyProfile(symbol);
        break;
      default:
        return NextResponse.json({ error: 'Invalid type' }, { status: 400 });
    }

    // Cache the result
    await redis.setEx(cacheKey, 60, JSON.stringify(data));

    return NextResponse.json(data, {
      headers: {
        'X-Cache': 'MISS',
        'X-Cache-TTL': '60'
      }
    });

  } catch (error) {
    console.error(`Error fetching stock data for ${symbol}:`, error);
    return NextResponse.json(
      { error: 'Failed to fetch stock data' },
      { status: 500 }
    );
  }
}

async function getStockOverview(symbol: string) {
  // Kết hợp dữ liệu từ nhiều nguồn
  const [latestPrice, profile, historical] = await Promise.all([
    getLatestPrice(symbol),
    vnstockAPI.getCompanyProfile(symbol).catch(() => null),
    vnstockAPI.getHistoricalData(
      symbol,
      getDefaultFromDate(30),
      getToday(),
      '1D'
    ).catch(() => [])
  ]);

  return {
    symbol: symbol.toUpperCase(),
    price: latestPrice,
    profile,
    historical: historical.slice(-30), // 30 ngày gần nhất
    lastUpdated: new Date().toISOString()
  };
}

async function getLatestPrice(symbol: string) {
  // Ưu tiên lấy từ Redis (real-time từ WebSocket)
  const redis = await getRedisClient();
  const cached = await redis.get(`stock:${symbol.toUpperCase()}:latest`);
  
  if (cached) {
    return JSON.parse(cached);
  }

  // Fallback: query từ database
  const latest = await prisma.stockPrice.findFirst({
    where: { symbol: symbol.toUpperCase() },
    orderBy: { timestamp: 'desc' }
  });

  return latest;
}

function getToday(): string {
  return new Date().toISOString().split('T')[0];
}

function getDefaultFromDate(days: number = 365): string {
  const date = new Date();
  date.setDate(date.getDate() - days);
  return date.toISOString().split('T')[0];
}
```

#### Alert API

TypeScript Copy

```typescript
// app/api/alerts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';

const alertSchema = z.object({
  symbol: z.string().min(1).max(10),
  conditionType: z.enum([
    'PRICE_ABOVE', 
    'PRICE_BELOW', 
    'CHANGE_PERCENT_ABOVE',
    'CHANGE_PERCENT_BELOW',
    'VOLUME_ABOVE',
    'BREAKOUT_HIGH',
    'BREAKOUT_LOW'
  ]),
  threshold: z.number(),
  notificationChannels: z.array(z.enum(['web', 'email', 'telegram'])),
  expiryDate: z.string().datetime().optional()
});

// GET /api/alerts - Lấy danh sách cảnh báo của user
export async function GET(request: NextRequest) {
  const session = await getServerSession();
  
  if (!session?.user?.id) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const alerts = await prisma.alert.findMany({
    where: {
      userId: session.user.id,
      isActive: true
    },
    orderBy: { createdAt: 'desc' }
  });

  return NextResponse.json(alerts);
}

// POST /api/alerts - Tạo cảnh báo mới
export async function POST(request: NextRequest) {
  const session = await getServerSession();
  
  if (!session?.user?.id) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    const body = await request.json();
    const validated = alertSchema.parse(body);

    // Kiểm tra giới hạn số lượng alert (ví dụ: tối đa 50 alert/user)
    const count = await prisma.alert.count({
      where: { userId: session.user.id, isActive: true }
    });

    if (count >= 50) {
      return NextResponse.json(
        { error: 'Alert limit reached (max 50)' },
        { status: 400 }
      );
    }

    const alert = await prisma.alert.create({
      data: {
        userId: session.user.id,
        symbol: validated.symbol.toUpperCase(),
        conditionType: validated.conditionType,
        threshold: validated.threshold,
        notificationChannels: validated.notificationChannels,
        expiryDate: validated.expiryDate ? new Date(validated.expiryDate) : null,
        isActive: true
      }
    });

    // Thêm vào Redis để Alert Service kiểm tra real-time
    await addAlertToRedis(alert);

    return NextResponse.json(alert, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Invalid input', details: error.errors },
        { status: 400 }
      );
    }
    
    console.error('Error creating alert:', error);
    return NextResponse.json(
      { error: 'Failed to create alert' },
      { status: 500 }
    );
  }
}

// DELETE /api/alerts/:id
export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const session = await getServerSession();
  
  if (!session?.user?.id) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const alert = await prisma.alert.findFirst({
    where: { id: params.id, userId: session.user.id }
  });

  if (!alert) {
    return NextResponse.json({ error: 'Alert not found' }, { status: 404 });
  }

  await prisma.alert.update({
    where: { id: params.id },
    data: { isActive: false }
  });

  // Remove from Redis
  await removeAlertFromRedis(params.id);

  return NextResponse.json({ success: true });
}

async function addAlertToRedis(alert: any) {
  const { getRedisClient } = await import('@/lib/redis');
  const redis = await getRedisClient();
  
  // Lưu alert vào sorted set theo symbol để dễ query
  await redis.zAdd(`alerts:${alert.symbol}`, {
    score: Date.now(),
    value: JSON.stringify({
      id: alert.id,
      userId: alert.userId,
      conditionType: alert.conditionType,
      threshold: alert.threshold,
      channels: alert.notificationChannels
    })
  });
}

async function removeAlertFromRedis(alertId: string) {
  // Implementation tùy thuộc vào cách lưu trữ
}
```

### 1.5 Alert Processing Service

TypeScript Copy

```typescript
// services/alert-processor.ts
import { getRedisClient } from '@/lib/redis';
import { prisma } from '@/lib/prisma';
import { sendNotification } from './notification';

interface StockUpdate {
  symbol: string;
  lastPrice: number;
  change: number;
  changePercent: number;
  volume: number;
  high: number;
  low: number;
  timestamp: string;
}

export class AlertProcessor {
  private isRunning = false;

  async start() {
    if (this.isRunning) return;
    this.isRunning = true;

    const redis = await getRedisClient();
    const subscriber = redis.duplicate();

    // Subscribe to stock updates channel
    await subscriber.subscribe('stock:updates', (message) => {
      const update: StockUpdate = JSON.parse(message);
      this.processAlerts(update);
    });

    console.log('Alert processor started');
  }

  private async processAlerts(update: StockUpdate) {
    const redis = await getRedisClient();
    
    // Lấy tất cả alerts cho symbol này
    const alerts = await redis.zRange(`alerts:${update.symbol}`, 0, -1);
    
    if (!alerts.length) return;

    for (const alertJson of alerts) {
      const alert = JSON.parse(alertJson);
      
      if (await this.checkCondition(alert, update)) {
        await this.triggerAlert(alert, update);
      }
    }
  }

  private async checkCondition(alert: any, update: StockUpdate): Promise<boolean> {
    switch (alert.conditionType) {
      case 'PRICE_ABOVE':
        return update.lastPrice >= alert.threshold;
      
      case 'PRICE_BELOW':
        return update.lastPrice <= alert.threshold;
      
      case 'CHANGE_PERCENT_ABOVE':
        return update.changePercent >= alert.threshold;
      
      case 'CHANGE_PERCENT_BELOW':
        return update.changePercent <= alert.threshold;
      
      case 'VOLUME_ABOVE':
        return update.volume >= alert.threshold;
      
      case 'BREAKOUT_HIGH':
        // Cần lấy high của N phiên trước để so sánh
        const recentHigh = await this.getRecentHigh(update.symbol, 20);
        return update.lastPrice > recentHigh;
      
      case 'BREAKOUT_LOW':
        const recentLow = await this.getRecentLow(update.symbol, 20);
        return update.lastPrice < recentLow;
      
      default:
        return false;
    }
  }

  private async triggerAlert(alert: any, update: StockUpdate) {
    // Gửi notification
    await sendNotification({
      userId: alert.userId,
      channels: alert.channels,
      title: `🚨 Cảnh báo ${update.symbol}`,
      body: `${update.symbol} đã chạm ngưỡng ${alert.conditionType} tại giá ${update.lastPrice.toLocaleString('vi-VN')}`,
      data: {
        symbol: update.symbol,
        price: update.lastPrice,
        change: update.changePercent,
        alertId: alert.id
      }
    });

    // Cập nhật trạng thái alert trong database
    await prisma.alert.update({
      where: { id: alert.id },
      data: {
        triggeredAt: new Date(),
        isActive: false // Tắt alert sau khi trigger (hoặc giữ lại tùy yêu cầu)
      }
    });

    // Xóa khỏi Redis
    const redis = await getRedisClient();
    await redis.zRem(`alerts:${update.symbol}`, JSON.stringify(alert));
  }

  private async getRecentHigh(symbol: string, days: number): Promise<number> {
    const prices = await prisma.stockPrice.findMany({
      where: {
        symbol,
        timestamp: {
          gte: new Date(Date.now() - days * 24 * 60 * 60 * 1000)
        }
      },
      orderBy: { timestamp: 'desc' },
      take: days,
      select: { high: true }
    });

    return Math.max(...prices.map(p => p.high));
  }

  private async getRecentLow(symbol: string, days: number): Promise<number> {
    const prices = await prisma.stockPrice.findMany({
      where: {
        symbol,
        timestamp: {
          gte: new Date(Date.now() - days * 24 * 60 * 60 * 1000)
        }
      },
      orderBy: { timestamp: 'desc' },
      take: days,
      select: { low: true }
    });

    return Math.min(...prices.map(p => p.low));
  }
}

export const alertProcessor = new AlertProcessor();
```

***

## 🗄️ PHẦN 2: DATABASE SCHEMA & MIGRATIONS

### 2.1 Cấu Trúc Database

plain Copy

```
┌─────────────────────────────────────────────────────────────┐
│                    PostgreSQL + TimescaleDB                 │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐ │
│  │   stock_prices  │  │    trades       │  │   indices   │ │
│  │   (hypertable)  │  │   (hypertable)  │  │  (regular)  │ │
│  └─────────────────┘  └─────────────────┘  └─────────────┘ │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐ │
│  │     alerts      │  │  watchlists     │  │  portfolios │ │
│  │   (regular)     │  │   (regular)     │  │  (regular)  │ │
│  └─────────────────┘  └─────────────────┘  └─────────────┘ │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐ │
│  │  transactions   │  │    users        │  │ notifications│ │
│  │   (regular)     │  │   (regular)     │  │  (regular)  │ │
│  └─────────────────┘  └─────────────────┘  └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
```

### 2.2 Schema Đầy Đủ với Prisma

prisma Copy

```prisma
// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// ==================== USER MANAGEMENT ====================

model User {
  id            String    @id @default(uuid())
  email         String    @unique
  password      String    // hashed
  name          String?
  phone         String?
  telegramId    String?   @unique
  emailVerified DateTime?
  image         String?
  role          UserRole  @default(USER)
  isActive      Boolean   @default(true)
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt

  // Relations
  alerts        Alert[]
  watchlists    Watchlist[]
  portfolios    Portfolio[]
  notifications Notification[]
  sessions      Session[]

  @@map("users")
}

enum UserRole {
  USER
  PREMIUM
  ADMIN
}

model Session {
  id           String   @id @default(uuid())
  userId       String
  token        String   @unique
  expiresAt    DateTime
  createdAt    DateTime @default(now())

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("sessions")
}

// ==================== MARKET DATA ====================

// Giá chứng khoán theo thời gian (TimescaleDB hypertable)
model StockPrice {
  id        BigInt   @id @default(autoincrement())
  symbol    String   @db.VarChar(10)
  timestamp DateTime @db.Timestamptz(3)
  
  // OHLC
  open      Decimal  @db.Decimal(12, 2)
  high      Decimal  @db.Decimal(12, 2)
  low       Decimal  @db.Decimal(12, 2)
  close     Decimal  @db.Decimal(12, 2)
  
  // Volume & Value
  volume    BigInt
  value     BigInt
  
  // Bid/Ask (mảng 10 mức giá)
  bidPrice  Decimal[] @db.Decimal(12, 2)
  bidVolume BigInt[]
  askPrice  Decimal[] @db.Decimal(12, 2)
  askVolume BigInt[]
  
  // Thông tin thêm
  reference Decimal?  @db.Decimal(12, 2) // Giá tham chiếu
  ceiling   Decimal?  @db.Decimal(12, 2) // Giá trần
  floor     Decimal?  @db.Decimal(12, 2) // Giá sàn
  
  // Metadata
  source    String    @default("MBS") // MBS, VNDIRECT, SSI
  createdAt DateTime  @default(now())

  @@unique([symbol, timestamp])
  @@index([symbol, timestamp(sort: Desc)])
  @@index([timestamp])
  @@map("stock_prices")
}

// Chi tiết giao dịch (tape) - TimescaleDB hypertable
model Trade {
  id          BigInt   @id @default(autoincrement())
  symbol      String   @db.VarChar(10)
  timestamp   DateTime @db.Timestamptz(3)
  
  price       Decimal  @db.Decimal(12, 2)
  volume      BigInt
  side        TradeSide? // B: Buy, S: Sell, null: unknown
  
  // Thông tin giao dịch
  matchType   String?  @db.VarChar(10) // LO, MP, ATC, PLO, etc.
  orderId     String?  @db.VarChar(50) // ID để trace
  
  createdAt   DateTime @default(now())

  @@index([symbol, timestamp(sort: Desc)])
  @@index([timestamp])
  @@map("trades")
}

enum TradeSide {
  BUY
  SELL
}

// Chỉ số thị trường (VN-Index, VN30, etc.)
model MarketIndex {
  id          BigInt   @id @default(autoincrement())
  symbol      String   @db.VarChar(20) // VNINDEX, VN30, HNXINDEX, etc.
  timestamp   DateTime @db.Timestamptz(3)
  
  value       Decimal  @db.Decimal(12, 2)
  change      Decimal  @db.Decimal(12, 2)
  changePercent Decimal @db.Decimal(5, 2)
  
  // Thông tin thị trường
  advance     Int      // Số mã tăng
  decline     Int      // Số mã giảm
  unchanged   Int      // Số mã đứng giá
  totalVolume BigInt   // Tổng khối lượng
  totalValue  BigInt   // Tổng giá trị
  
  createdAt   DateTime @default(now())

  @@unique([symbol, timestamp])
  @@index([symbol, timestamp(sort: Desc)])
  @@map("market_indices")
}

// Thông tin công ty
model Company {
  id                String   @id @default(uuid())
  symbol            String   @unique @db.VarChar(10)
  
  // Thông tin cơ bản
  companyName       String
  shortName         String?
  industry          String?
  sector            String?
  exchange          String   @db.VarChar(10) // HOSE, HNX, UPCOM
  
  // Thông tin tài chính (cập nhật định kỳ)
  marketCap         BigInt?
  sharesOutstanding BigInt?
  eps               Decimal? @db.Decimal(12, 2)
  pe                Decimal? @db.Decimal(8, 2)
  pb                Decimal? @db.Decimal(8, 2)
  roe               Decimal? @db.Decimal(5, 2)
  roa               Decimal? @db.Decimal(5, 2)
  dividendYield     Decimal? @db.Decimal(5, 2)
  
  // Thông tin liên hệ
  website           String?
  address           String?
  employees         Int?
  
  // Metadata
  lastUpdated       DateTime @updatedAt
  createdAt         DateTime @default(now())

  @@map("companies")
}

// ==================== ALERT SYSTEM ====================

model Alert {
  id                   String   @id @default(uuid())
  userId               String
  
  // Cấu hình alert
  symbol               String   @db.VarChar(10)
  conditionType        AlertCondition
  threshold            Decimal  @db.Decimal(12, 4)
  
  // Thông báo
  notificationChannels NotificationChannel[]
  message              String?
  
  // Trạng thái
  isActive             Boolean  @default(true)
  triggeredAt          DateTime?
  triggerCount         Int      @default(0)
  maxTriggers          Int      @default(1) // Số lần trigger tối đa
  
  // Thời hạn
  expiryDate           DateTime?
  
  createdAt            DateTime @default(now())
  updatedAt            DateTime @updatedAt

  // Relations
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, isActive])
  @@index([symbol, isActive])
  @@map("alerts")
}

enum AlertCondition {
  PRICE_ABOVE
  PRICE_BELOW
  CHANGE_PERCENT_ABOVE
  CHANGE_PERCENT_BELOW
  VOLUME_ABOVE
  BREAKOUT_HIGH
  BREAKOUT_LOW
  RSI_ABOVE
  RSI_BELOW
}

enum NotificationChannel {
  WEB
  EMAIL
  TELEGRAM
  SMS
}

// Lịch sử alert đã trigger
model AlertHistory {
  id        String   @id @default(uuid())
  alertId   String
  userId    String
  
  // Dữ liệu khi trigger
  symbol    String   @db.VarChar(10)
  price     Decimal  @db.Decimal(12, 2)
  value     Decimal  @db.Decimal(12, 4) // Giá trị tại thời điểm trigger
  
  // Thông báo đã gửi
  channels  NotificationChannel[]
  sentAt    DateTime
  
  createdAt DateTime @default(now())

  @@index([userId, createdAt(sort: Desc)])
  @@map("alert_history")
}

// ==================== WATCHLIST & PORTFOLIO ====================

model Watchlist {
  id          String   @id @default(uuid())
  userId      String
  name        String
  description String?
  isDefault   Boolean  @default(false)
  order       Int      @default(0)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  // Relations
  user   User            @relation(fields: [userId], references: [id], onDelete: Cascade)
  items  WatchlistItem[]

  @@unique([userId, name])
  @@map("watchlists")
}

model WatchlistItem {
  id          String   @id @default(uuid())
  watchlistId String
  symbol      String   @db.VarChar(10)
  order       Int      @default(0)
  notes       String?
  addedAt     DateTime @default(now())

  watchlist Watchlist @relation(fields: [watchlistId], references: [id], onDelete: Cascade)

  @@unique([watchlistId, symbol])
  @@map("watchlist_items")
}

// Danh mục đầu tư
model Portfolio {
  id          String   @id @default(uuid())
  userId      String
  name        String
  description String?
  currency    String   @default("VND")
  isDefault   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  // Relations
  user         User          @relation(fields: [userId], references: [id], onDelete: Cascade)
  holdings     Holding[]
  transactions Transaction[]

  @@map("portfolios")
}

// Vị thế nắm giữ
model Holding {
  id          String   @id @default(uuid())
  portfolioId String
  symbol      String   @db.VarChar(10)
  
  // Số lượng
  quantity    Decimal  @db.Decimal(15, 2)
  averageCost Decimal  @db.Decimal(12, 2)
  
  // Thông tin cập nhật
  lastPrice   Decimal? @db.Decimal(12, 2)
  marketValue Decimal? @db.Decimal(15, 2)
  unrealizedPnl Decimal? @db.Decimal(15, 2)
  unrealizedPnlPercent Decimal? @db.Decimal(5, 2)
  
  updatedAt   DateTime @updatedAt
  createdAt   DateTime @default(now())

  portfolio Portfolio @relation(fields: [portfolioId], references: [id], onDelete: Cascade)

  @@unique([portfolioId, symbol])
  @@map("holdings")
}

// Lịch sử giao dịch
model Transaction {
  id          String   @id @default(uuid())
  portfolioId String
  symbol      String   @db.VarChar(10)
  
  type        TransactionType
  quantity    Decimal  @db.Decimal(15, 2)
  price       Decimal  @db.Decimal(12, 2)
  fees        Decimal  @db.Decimal(12, 2) @default(0)
  taxes       Decimal  @db.Decimal(12, 2) @default(0)
  total       Decimal  @db.Decimal(15, 2)
  
  date        DateTime @db.Date
  notes       String?
  
  createdAt   DateTime @default(now())

  portfolio Portfolio @relation(fields: [portfolioId], references: [id], onDelete: Cascade)

  @@index([portfolioId, date(sort: Desc)])
  @@map("transactions")
}

enum TransactionType {
  BUY
  SELL
  DIVIDEND
  SPLIT
  BONUS
  RIGHTS
}

// ==================== NOTIFICATIONS ====================

model Notification {
  id        String   @id @default(uuid())
  userId    String
  
  type      NotificationType
  title     String
  body      String
  data      Json?    // Additional payload
  
  isRead    Boolean  @default(false)
  readAt    DateTime?
  
  createdAt DateTime @default(now())

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, isRead])
  @@index([userId, createdAt(sort: Desc)])
  @@map("notifications")
}

enum NotificationType {
  ALERT
  PRICE_UPDATE
  SYSTEM
  NEWS
}

// ==================== SYSTEM ====================

// Cấu hình hệ thống
model SystemConfig {
  id        String   @id @default(uuid())
  key       String   @unique
  value     Json
  updatedAt DateTime @updatedAt
  createdAt DateTime @default(now())

  @@map("system_config")
}

// Log lỗi và sự kiện
model SystemLog {
  id        BigInt   @id @default(autoincrement())
  level     LogLevel
  service   String   @db.VarChar(50)
  message   String
  metadata  Json?
  timestamp DateTime @default(now())

  @@index([timestamp(sort: Desc)])
  @@index([level, timestamp(sort: Desc)])
  @@map("system_logs")
}

enum LogLevel {
  DEBUG
  INFO
  WARN
  ERROR
  FATAL
}
```

### 2.3 Migration Scripts

#### Migration 001: Initial Schema

sql Copy

```sql
-- migrations/001_initial_schema.sql

-- Enable TimescaleDB extension
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- ==================== USER MANAGEMENT ====================

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    phone VARCHAR(20),
    telegram_id VARCHAR(50) UNIQUE,
    email_verified TIMESTAMPTZ,
    image VARCHAR(500),
    role VARCHAR(20) DEFAULT 'USER' CHECK (role IN ('USER', 'PREMIUM', 'ADMIN')),
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token VARCHAR(255) UNIQUE NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- ==================== MARKET DATA ====================

CREATE TABLE stock_prices (
    id BIGSERIAL,
    symbol VARCHAR(10) NOT NULL,
    timestamp TIMESTAMPTZ NOT NULL,
    open DECIMAL(12, 2) NOT NULL,
    high DECIMAL(12, 2) NOT NULL,
    low DECIMAL(12, 2) NOT NULL,
    close DECIMAL(12, 2) NOT NULL,
    volume BIGINT NOT NULL,
    value BIGINT NOT NULL,
    bid_price DECIMAL(12, 2)[] DEFAULT '{}',
    bid_volume BIGINT[] DEFAULT '{}',
    ask_price DECIMAL(12, 2)[] DEFAULT '{}',
    ask_volume BIGINT[] DEFAULT '{}',
    reference DECIMAL(12, 2),
    ceiling DECIMAL(12, 2),
    floor DECIMAL(12, 2),
    source VARCHAR(20) DEFAULT 'MBS',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    
    PRIMARY KEY (symbol, timestamp)
);

-- Convert to hypertable for time-series data
SELECT create_hypertable('stock_prices', 'timestamp', 
    chunk_time_interval => INTERVAL '1 day',
    if_not_exists => TRUE
);

-- Indexes for stock_prices
CREATE INDEX idx_stock_prices_symbol_time_desc ON stock_prices (symbol, timestamp DESC);
CREATE INDEX idx_stock_prices_timestamp ON stock_prices (timestamp DESC);

CREATE TABLE trades (
    id BIGSERIAL,
    symbol VARCHAR(10) NOT NULL,
    timestamp TIMESTAMPTZ NOT NULL,
    price DECIMAL(12, 2) NOT NULL,
    volume BIGINT NOT NULL,
    side VARCHAR(4) CHECK (side IN ('BUY', 'SELL')),
    match_type VARCHAR(10),
    order_id VARCHAR(50),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    
    PRIMARY KEY (id, timestamp)
);

-- Convert to hypertable
SELECT create_hypertable('trades', 'timestamp',
    chunk_time_interval => INTERVAL '1 day',
    if_not_exists => TRUE
);

CREATE INDEX idx_trades_symbol_time ON trades (symbol, timestamp DESC);

CREATE TABLE market_indices (
    id BIGSERIAL,
    symbol VARCHAR(20) NOT NULL,
    timestamp TIMESTAMPTZ NOT NULL,
    value DECIMAL(12, 2) NOT NULL,
    change DECIMAL(12, 2) NOT NULL,
    change_percent DECIMAL(5, 2) NOT NULL,
    advance INT DEFAULT 0,
    decline INT DEFAULT 0,
    unchanged INT DEFAULT 0,
    total_volume BIGINT DEFAULT 0,
    total_value BIGINT DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    
    PRIMARY KEY (symbol, timestamp)
);

SELECT create_hypertable('market_indices', 'timestamp',
    chunk_time_interval => INTERVAL '1 day',
    if_not_exists => TRUE
);

CREATE TABLE companies (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    symbol VARCHAR(10) UNIQUE NOT NULL,
    company_name VARCHAR(255) NOT NULL,
    short_name VARCHAR(100),
    industry VARCHAR(100),
    sector VARCHAR(100),
    exchange VARCHAR(10) NOT NULL CHECK (exchange IN ('HOSE', 'HNX', 'UPCOM')),
    market_cap BIGINT,
    shares_outstanding BIGINT,
    eps DECIMAL(12, 2),
    pe DECIMAL(8, 2),
    pb DECIMAL(8, 2),
    roe DECIMAL(5, 2),
    roa DECIMAL(5, 2),
    dividend_yield DECIMAL(5, 2),
    website VARCHAR(255),
    address TEXT,
    employees INT,
    last_updated TIMESTAMPTZ DEFAULT NOW(),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- ==================== ALERT SYSTEM ====================

CREATE TYPE alert_condition AS ENUM (
    'PRICE_ABOVE', 'PRICE_BELOW', 
    'CHANGE_PERCENT_ABOVE', 'CHANGE_PERCENT_BELOW',
    'VOLUME_ABOVE', 'BREAKOUT_HIGH', 'BREAKOUT_LOW',
    'RSI_ABOVE', 'RSI_BELOW'
);

CREATE TYPE notification_channel AS ENUM ('WEB', 'EMAIL', 'TELEGRAM', 'SMS');

CREATE TABLE alerts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    symbol VARCHAR(10) NOT NULL,
    condition_type alert_condition NOT NULL,
    threshold DECIMAL(12, 4) NOT NULL,
    notification_channels notification_channel[] DEFAULT '{}',
    message TEXT,
    is_active BOOLEAN DEFAULT true,
    triggered_at TIMESTAMPTZ,
    trigger_count INT DEFAULT 0,
    max_triggers INT DEFAULT 1,
    expiry_date TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_alerts_user_active ON alerts(user_id, is_active);
CREATE INDEX idx_alerts_symbol_active ON alerts(symbol, is_active);

CREATE TABLE alert_history (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    alert_id UUID NOT NULL,
    user_id UUID NOT NULL,
    symbol VARCHAR(10) NOT NULL,
    price DECIMAL(12, 2) NOT NULL,
    value DECIMAL(12, 4) NOT NULL,
    channels notification_channel[] DEFAULT '{}',
    sent_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_alert_history_user ON alert_history(user_id, created_at DESC);

-- ==================== WATCHLIST & PORTFOLIO ====================

CREATE TABLE watchlists (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    is_default BOOLEAN DEFAULT false,
    "order" INT DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    
    UNIQUE(user_id, name)
);

CREATE TABLE watchlist_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    watchlist_id UUID NOT NULL REFERENCES watchlists(id) ON DELETE CASCADE,
    symbol VARCHAR(10) NOT NULL,
    "order" INT DEFAULT 0,
    notes TEXT,
    added_at TIMESTAMPTZ DEFAULT NOW(),
    
    UNIQUE(watchlist_id, symbol)
);

CREATE TABLE portfolios (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    currency VARCHAR(3) DEFAULT 'VND',
    is_default BOOLEAN DEFAULT false,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TYPE transaction_type AS ENUM ('BUY', 'SELL', 'DIVIDEND', 'SPLIT', 'BONUS', 'RIGHTS');

CREATE TABLE holdings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
    symbol VARCHAR(10) NOT NULL,
    quantity DECIMAL(15, 2) NOT NULL,
    average_cost DECIMAL(12, 2) NOT NULL,
    last_price DECIMAL(12, 2),
    market_value DECIMAL(15, 2),
    unrealized_pnl DECIMAL(15, 2),
    unrealized_pnl_percent DECIMAL(5, 2),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    
    UNIQUE(portfolio_id, symbol)
);

CREATE TABLE transactions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
    symbol VARCHAR(10) NOT NULL,
    type transaction_type NOT NULL,
    quantity DECIMAL(15, 2) NOT NULL,
    price DECIMAL(12, 2) NOT NULL,
    fees DECIMAL(12, 2) DEFAULT 0,
    taxes DECIMAL(12, 2) DEFAULT 0,
    total DECIMAL(15, 2) NOT NULL,
    date DATE NOT NULL,
    notes TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_transactions_portfolio_date ON transactions(portfolio_id, date DESC);

-- ==================== NOTIFICATIONS ====================

CREATE TYPE notification_type AS ENUM ('ALERT', 'PRICE_UPDATE', 'SYSTEM', 'NEWS');

CREATE TABLE notifications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    type notification_type NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL,
    data JSONB,
    is_read BOOLEAN DEFAULT false,
    read_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_notifications_user_unread ON notifications(user_id, is_read);
CREATE INDEX idx_notifications_user_created ON notifications(user_id, created_at DESC);

-- ==================== SYSTEM ====================

CREATE TABLE system_config (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    key VARCHAR(100) UNIQUE NOT NULL,
    value JSONB NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TYPE log_level AS ENUM ('DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL');

CREATE TABLE system_logs (
    id BIGSERIAL PRIMARY KEY,
    level log_level NOT NULL,
    service VARCHAR(50) NOT NULL,
    message TEXT NOT NULL,
    metadata JSONB,
    timestamp TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_system_logs_timestamp ON system_logs(timestamp DESC);
CREATE INDEX idx_system_logs_level_timestamp ON system_logs(level, timestamp DESC);

-- Continuous aggregates for fast queries
CREATE MATERIALIZED VIEW stock_prices_1h
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', timestamp) AS bucket,
    symbol,
    first(open, timestamp) as open,
    max(high) as high,
    min(low) as low,
    last(close, timestamp) as close,
    sum(volume) as volume,
    sum(value) as value
FROM stock_prices
GROUP BY bucket, symbol;

-- Retention policy (giữ data 1 năm cho raw data, aggregate giữ lâu hơn)
SELECT add_retention_policy('stock_prices', INTERVAL '1 year');
SELECT add_retention_policy('trades', INTERVAL '6 months');
```

#### Migration 002: Add Functions & Triggers

sql Copy

```sql
-- migrations/002_functions_triggers.sql

-- Function để tự động cập nhật updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

-- Apply cho các bảng cần updated_at
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_alerts_updated_at BEFORE UPDATE ON alerts
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_watchlists_updated_at BEFORE UPDATE ON watchlists
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_portfolios_updated_at BEFORE UPDATE ON portfolios
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_holdings_updated_at BEFORE UPDATE ON holdings
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

-- Function để tính toán P&L tự động khi cập nhật holding
CREATE OR REPLACE FUNCTION calculate_holding_pnl()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.last_price IS NOT NULL THEN
        NEW.market_value := NEW.quantity * NEW.last_price;
        NEW.unrealized_pnl := (NEW.last_price - NEW.average_cost) * NEW.quantity;
        
        IF NEW.average_cost > 0 THEN
            NEW.unrealized_pnl_percent := ((NEW.last_price - NEW.average_cost) / NEW.average_cost) * 100;
        END IF;
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_calculate_holding_pnl
    BEFORE INSERT OR UPDATE ON holdings
    FOR EACH ROW
    EXECUTE FUNCTION calculate_holding_pnl();

-- Function để tính total khi insert transaction
CREATE OR REPLACE FUNCTION calculate_transaction_total()
RETURNS TRIGGER AS $$
BEGIN
    NEW.total := (NEW.quantity * NEW.price) + NEW.fees + NEW.taxes;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_calculate_transaction_total
    BEFORE INSERT ON transactions
    FOR EACH ROW
    EXECUTE FUNCTION calculate_transaction_total();

-- Function để cập nhật holding khi có transaction mới
CREATE OR REPLACE FUNCTION update_holding_on_transaction()
RETURNS TRIGGER AS $$
DECLARE
    existing_holding_id UUID;
    current_qty DECIMAL(15,2);
    current_avg DECIMAL(12,2);
BEGIN
    -- Tìm holding hiện tại
    SELECT id, quantity, average_cost 
    INTO existing_holding_id, current_qty, current_avg
    FROM holdings 
    WHERE portfolio_id = NEW.portfolio_id AND symbol = NEW.symbol;
    
    IF NEW.type = 'BUY' THEN
        IF existing_holding_id IS NOT NULL THEN
            -- Cập nhật holding hiện có
            UPDATE holdings SET
                quantity = current_qty + NEW.quantity,
                average_cost = ((current_qty * current_avg) + (NEW.quantity * NEW.price)) / (current_qty + NEW.quantity),
                updated_at = NOW()
            WHERE id = existing_holding_id;
        ELSE
            -- Tạo holding mới
            INSERT INTO holdings (portfolio_id, symbol, quantity, average_cost)
            VALUES (NEW.portfolio_id, NEW.symbol, NEW.quantity, NEW.price);
        END IF;
        
    ELSIF NEW.type = 'SELL' THEN
        IF existing_holding_id IS NOT NULL THEN
            IF current_qty <= NEW.quantity THEN
                -- Xóa holding nếu bán hết
                DELETE FROM holdings WHERE id = existing_holding_id;
            ELSE
                -- Giảm số lượng (không đổi average cost)
                UPDATE holdings SET
                    quantity = current_qty - NEW.quantity,
                    updated_at = NOW()
                WHERE id = existing_holding_id;
            END IF;
        END IF;
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_update_holding_on_transaction
    AFTER INSERT ON transactions
    FOR EACH ROW
    EXECUTE FUNCTION update_holding_on_transaction();

-- Function để lấy top movers
CREATE OR REPLACE FUNCTION get_top_movers(
    p_exchange VARCHAR(10) DEFAULT NULL,
    p_limit INT DEFAULT 10,
    p_order_by VARCHAR(20) DEFAULT 'change_percent' -- 'change_percent', 'volume', 'value'
)
RETURNS TABLE (
    symbol VARCHAR(10),
    company_name VARCHAR(255),
    last_price DECIMAL(12,2),
    change DECIMAL(12,2),
    change_percent DECIMAL(5,2),
    volume BIGINT,
    value BIGINT,
    market_cap BIGINT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        sp.symbol,
        c.company_name,
        sp.close as last_price,
        sp.close - sp.reference as change,
        ((sp.close - sp.reference) / sp.reference * 100)::DECIMAL(5,2) as change_percent,
        sp.volume,
        sp.value,
        c.market_cap
    FROM stock_prices sp
    JOIN companies c ON sp.symbol = c.symbol
    WHERE sp.timestamp = (
        SELECT MAX(timestamp) FROM stock_prices
    )
    AND (p_exchange IS NULL OR c.exchange = p_exchange)
    ORDER BY 
        CASE p_order_by
            WHEN 'change_percent' THEN ABS((sp.close - sp.reference) / sp.reference * 100)
            WHEN 'volume' THEN sp.volume
            WHEN 'value' THEN sp.value
        END DESC
    LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;

-- Function để lấy market overview
CREATE OR REPLACE FUNCTION get_market_overview()
RETURNS TABLE (
    exchange VARCHAR(10),
    index_value DECIMAL(12,2),
    index_change DECIMAL(12,2),
    index_change_percent DECIMAL(5,2),
    advance INT,
    decline INT,
    unchanged INT,
    total_volume BIGINT,
    total_value BIGINT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        CASE 
            WHEN mi.symbol = 'VNINDEX' THEN 'HOSE'
            WHEN mi.symbol = 'HNXINDEX' THEN 'HNX'
            WHEN mi.symbol = 'UPCOMINDEX' THEN 'UPCOM'
        END as exchange,
        mi.value as index_value,
        mi.change as index_change,
        mi.change_percent as index_change_percent,
        mi.advance,
        mi.decline,
        mi.unchanged,
        mi.total_volume,
        mi.total_value
    FROM market_indices mi
    WHERE mi.timestamp = (
        SELECT MAX(timestamp) FROM market_indices
    )
    AND mi.symbol IN ('VNINDEX', 'HNXINDEX', 'UPCOMINDEX');
END;
$$ LANGUAGE plpgsql;
```

#### Migration 003: Seed Data

sql Copy

```sql
-- migrations/003_seed_data.sql

-- Insert các công ty mẫu (top cổ phiếu VN30)
INSERT INTO companies (symbol, company_name, short_name, industry, sector, exchange) VALUES
('VNM', 'Vinamilk', 'Vinamilk', 'Thực phẩm', 'Tiêu dùng', 'HOSE'),
('VIC', 'Vingroup', 'Vingroup', 'Bất động sản', 'Bất động sản', 'HOSE'),
('HPG', 'Hòa Phát', 'Hòa Phát', 'Thép', 'Vật liệu', 'HOSE'),
('MWG', 'Thế Giới Di Động', 'TGDD', 'Bán lẻ', 'Tiêu dùng', 'HOSE'),
('FPT', 'FPT Corporation', 'FPT', 'Công nghệ', 'Công nghệ', 'HOSE'),
('VCB', 'Vietcombank', 'Vietcombank', 'Ngân hàng', 'Tài chính', 'HOSE'),
('VHM', 'Vinhomes', 'Vinhomes', 'Bất động sản', 'Bất động sản', 'HOSE'),
('GAS', 'PV Gas', 'PV Gas', 'Dầu khí', 'Năng lượng', 'HOSE'),
('MSN', 'Masan Group', 'Masan', 'Tiêu dùng', 'Tiêu dùng', 'HOSE'),
('TCH', 'Techcombank', 'Techcombank', 'Ngân hàng', 'Tài chính', 'HOSE');

-- Insert system config mặc định
INSERT INTO system_config (key, value) VALUES
('market_hours', '{"open": "09:00", "close": "15:00", "lunch_start": "11:30", "lunch_end": "13:00"}'::jsonb),
('data_sources', '{"primary": "MBS", "fallbacks": ["VNDIRECT", "SSI"]}'::jsonb),
('alert_limits', '{"free": 5, "premium": 50}'::jsonb),
('websocket_config', '{"reconnect_interval": 5000, "max_reconnect": 10}'::jsonb);

-- Insert sample data cho testing (giả lập 1 ngày giao dịch)
DO $$
DECLARE
    v_date DATE := CURRENT_DATE;
    v_time TIME;
    v_base_price DECIMAL(12,2);
    v_price DECIMAL(12,2);
BEGIN
    -- Tạo dữ liệu cho VNM
    v_base_price := 78500;
    v_time := '09:00:00';
    
    WHILE v_time <= '15:00:00' LOOP
        -- Random walk price
        v_price := v_base_price + (random() - 0.5) * 1000;
        
        INSERT INTO stock_prices (
            symbol, timestamp, open, high, low, close, 
            volume, value, bid_price, bid_volume, ask_price, ask_volume,
            reference, ceiling, floor
        ) VALUES (
            'VNM',
            v_date + v_time,
            v_price - 50,
            v_price + 100,
            v_price - 100,
            v_price,
            (random() * 100000)::BIGINT,
            (random() * 1000000000)::BIGINT,
            ARRAY[v_price-100, v_price-200, v_price-300],
            ARRAY[(random()*1000)::BIGINT, (random()*1000)::BIGINT, (random()*1000)::BIGINT],
            ARRAY[v_price+100, v_price+200, v_price+300],
            ARRAY[(random()*1000)::BIGINT, (random()*1000)::BIGINT, (random()*1000)::BIGINT],
            78500,
            86300,
            70700
        );
        
        v_time := v_time + INTERVAL '5 minutes';
        v_base_price := v_price;
    END LOOP;
END $$;
```

### 2.4 Docker Compose Setup

yaml Copy

```yaml
# docker-compose.yml
version: '3.8'

services:
  postgres:
    image: timescale/timescaledb:latest-pg15
    container_name: mbs_tracker_db
    environment:
      POSTGRES_USER: mbs_user
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secure_password_123}
      POSTGRES_DB: mbs_tracker
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./migrations:/docker-entrypoint-initdb.d
    command: >
      postgres 
      -c shared_preload_libraries=timescaledb
      -c max_connections=200
      -c shared_buffers=2GB
      -c effective_cache_size=6GB
      -c maintenance_work_mem=512MB
      -c checkpoint_completion_target=0.9
      -c wal_buffers=16MB
      -c default_statistics_target=100
      -c random_page_cost=1.1
      -c effective_io_concurrency=200
      -c work_mem=5242kB
      -c min_wal_size=1GB
      -c max_wal_size=4GB
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mbs_user -d mbs_tracker"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: mbs_tracker_redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

  # Optional: Grafana for monitoring
  grafana:
    image: grafana/grafana:latest
    container_name: mbs_tracker_grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - postgres

volumes:
  postgres_data:
  redis_data:
  grafana_data:
```

### 2.5 Database Connection & Client Setup

TypeScript Copy

```typescript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' 
    ? ['query', 'error', 'warn'] 
    : ['error'],
});

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

// Connection pool settings
export const prismaExtended = prisma.$extends({
  query: {
    $allModels: {
      async findMany({ model, operation, args, query }) {
        // Add query timeout
        const start = Date.now();
        const result = await query(args);
        const duration = Date.now() - start;
        
        if (duration > 1000) {
          console.warn(`Slow query detected: ${model}.${operation} took ${duration}ms`);
        }
        
        return result;
      },
    },
  },
});
```

TypeScript Copy

```typescript
// lib/redis.ts
import { createClient, RedisClientType } from 'redis';

let redisClient: RedisClientType | null = null;

export async function getRedisClient(): Promise<RedisClientType> {
  if (!redisClient) {
    redisClient = createClient({
      url: process.env.REDIS_URL || 'redis://localhost:6379',
      socket: {
        reconnectStrategy: (retries) => {
          if (retries > 10) {
            console.error('Max redis reconnection attempts reached');
            return new Error('Max retries');
          }
          return Math.min(retries * 100, 3000);
        },
      },
    });

    redisClient.on('error', (err) => console.error('Redis Client Error', err));
    redisClient.on('connect', () => console.log('Redis Client Connected'));
    
    await redisClient.connect();
  }
  
  return redisClient;
}

// Helper functions cho common operations
export async function getCachedStock(symbol: string) {
  const client = await getRedisClient();
  const data = await client.get(`stock:${symbol.toUpperCase()}:latest`);
  return data ? JSON.parse(data) : null;
}

export async function setCachedStock(symbol: string, data: any, ttl: number = 60) {
  const client = await getRedisClient();
  await client.setEx(`stock:${symbol.toUpperCase()}:latest`, ttl, JSON.stringify(data));
}

export async function publishStockUpdate(data: any) {
  const client = await getRedisClient();
  await client.publish('stock:updates', JSON.stringify(data));
}
```

***

Đây là toàn bộ spec kỹ thuật chi tiết cho hệ thống. Bạn cần tôi giải thích thêm phần nào hoặc triển khai thêm tính năng nào không?


# YouTube RAG Scraper

### 🎯 Tổng quan dự án

**youtube-rag-scraper** là một pipeline CLI mã nguồn mở giúp biến toàn bộ kênh YouTube thành dataset sẵn sàng cho hệ thống RAG (Retrieval-Augmented Generation). Đây là giải pháp end-to-end từ video YouTube → vector index để semantic search.

***

### 🏗️ Kiến trúc Pipeline

Từ sơ đồ bạn cung cấp, pipeline hoạt động như sau:plain Copy

```
┌─────────────────┐
│  🎬 YouTube     │ ← Channel / Playlist / Video URL
│     Input       │
└────────┬────────┘
         ▼
┌─────────────────┐
│   main.py       │ ← CLI Orchestrator (điều phối chính)
│  CLI Orchestrator│
└────────┬────────┘
         ├──────────────────┐
         ▼                  ▼
┌─────────────────┐  ┌─────────────────┐
│  metadata.py    │  │  transcripts.py │
│ YouTube Data API│  │   yt-dlp Engine │
│     v3          │  │                 │
└────────┬────────┘  └────────┬────────┘
         │                      │
         ▼                      ▼
    Video IDs +            VTT → Plain Text
    Metadata                    │
         └──────────┬──────────┘
                    ▼
            ┌─────────────────┐
            │    utils.py     │
            │  Data Pipeline  │
            │  (Làm sạch &    │
            │   Chunking)     │
            └────────┬────────┘
                     ├──────────────────┐
                     ▼                  ▼
            ┌─────────────────┐  ┌─────────────────┐
            │  JSON/JSONL/CSV │  │   RAG Chunks    │
            │    /Parquet     │  │                 │
            └────────┬────────┘  └────────┬────────┘
                     ▼                  ▼
            ┌─────────────────┐  ┌─────────────────┐
            │  📁 Dataset     │  │ knowledge_base.py│
            │     Files       │  │Embedding Generator│
            └─────────────────┘  └────────┬────────┘
                                          ▼
                              ┌─────────────────┐
                              │ sentence-       │
                              │ transformers    │
                              │  all-MiniLM-L6-v2│
                              └────────┬────────┘
                                       ▼
                              ┌─────────────────┐
                              │  384-dim vectors │
                              └────────┬────────┘
                                       ▼
                              ┌─────────────────┐
                              │   🟨 FAISS      │
                              │     Index       │
                              └────────┬────────┘
                                       ▼
                              ┌─────────────────┐
                              │   search.py     │
                              │ Semantic Search │
                              └────────┬────────┘
                                       ▼
                              ┌─────────────────┐
                              │  🔍 Natural     │
                              │ Language Query  │
                              │   (Top-K Results)│
                              └─────────────────┘
```

***

### 🔧 Các thành phần chính

Table

| Component                 | Công nghệ                                | Chức năng                                      |
| ------------------------- | ---------------------------------------- | ---------------------------------------------- |
| **Metadata Extraction**   | YouTube Data API v3                      | Lấy danh sách video, tiêu đề, mô tả, ngày đăng |
| **Transcript Extraction** | yt-dlp Engine                            | Tải subtitle/caption (VTT → Plain Text)        |
| **Data Processing**       | utils.py                                 | Làm sạch, chunking, xử lý pipeline             |
| **Embedding**             | sentence-transformers (all-MiniLM-L6-v2) | Tạo vector 384 chiều                           |
| **Vector Store**          | FAISS Index                              | Lưu trữ và tìm kiếm vector nhanh               |
| **Search**                | search.py                                | Semantic search, trả về Top-K results          |

***

### 📊 Output cuối cùng

1. **Dataset Files** (JSON/JSONL/CSV/Parquet): Chứa transcript đã chunk
2. **Embedding Matrix**: Ma trận vector 384 chiều
3. **FAISS Index**: Index vector để semantic search cực nhanh

***

### 🚀 Use Cases điển hình

* **"Chat với một kênh YouTube"**: Tạo chatbot trả lời dựa trên toàn bộ nội dung kênh
* **AI Assistant cho khóa học**: Học viên hỏi đáp từ hàng trăm video hướng dẫn
* **Domain-specific RAG**: Xây dựng knowledge base từ podcast, talkshow chuyên ngành
* **Nghiên cứu nội dung**: Phân tích xu hướng, trích xuất thông tin từ video dài

***

### 🤔 Câu hỏi kỹ thuật thú vị

Dự án đặt ra những trade-off quan trọng cho RAG pipeline:Table

| Câu hỏi               | Lựa chọn                                      | Trade-off                                                     |
| --------------------- | --------------------------------------------- | ------------------------------------------------------------- |
| **Chunking strategy** | Timestamp-based vs Semantic chunking          | Timestamp giữ ngữ cảnh thời gian, semantic giữ ý nghĩa chủ đề |
| **Synthetic Q\&A**    | Tạo Q\&A giả lập trước khi embedding?         | Cải thiện retrieval nhưng tăng chi phí tính toán              |
| **Vector DB**         | FAISS local vs Vector DB (Pinecone, Weaviate) | FAISS miễn phí, nhẹ; Vector DB scalable nhưng có phí          |

***

### 💡 Điểm mạnh của pipeline

1. **Modular**: Tách biệt rõ ràng giữa metadata, transcript, processing, embedding
2. **Local-first**: Dùng FAISS + sentence-transformers, không phụ thuộc API ngoài (trừ YouTube API)
3. **Scalable**: Hỗ trợ xử lý hàng trăm video (như log trong ảnh: 340 videos với 4 workers)
4. **Output đa dạng**: Hỗ trợ nhiều format dataset (JSON, Parquet, CSV)

***

### 📈 So sánh với các giải pháp khác

Table

| Đặc điểm      | youtube-rag-scraper | Youtube-comment-RAG | Apify Transcript Extractor |
| ------------- | ------------------- | ------------------- | -------------------------- |
| **Mã nguồn**  | Open source         | Open source         | Commercial (Apify)         |
| **Embedding** | Local (MiniLM)      | Gemini API          | Không tích hợp sẵn         |
| **Vector DB** | FAISS (local)       | Không có sẵn        | Không có sẵn               |
| **Chunking**  | Có                  | Không rõ            | Có (configurable)          |
| **Chi phí**   | Miễn phí            | Gemini API fee      | $0.15/1000 videos          |

***

### 🔗 Tài nguyên liên quan

Nếu bạn quan tâm đến việc mở rộng pipeline này, có thể tham khảo:

* [**Neo4j Knowledge Graph approach**](https://neo4j.com/blog/developer/youtube-transcripts-knowledge-graphs-rag/) : Kết hợp vector search với graph database
* [**CrewAI YoutubeVideoSearchTool**](https://docs.crewai.com/en/tools/search-research/youtubevideosearchtool) : Tích hợp vào agent framework
* [**youtube-transcript-rag**](https://github.com/aliahmad552/youtube-transcript-rag) : FastAPI + HuggingFace cho Q\&A chatbot

Dự án này là một ví dụ điển hình về cách biến nội dung video không cấu trúc thành dữ liệu có cấu trúc sẵn sàng cho AI - rất phù hợp cho các ứng dụng RAG thực tế!


# Welcome

Welcome to the GitBook starter template! Here you'll get an overview of all the amazing features GitBook offers to help you build beautiful, interactive documentation.

You'll see some of the best parts of GitBook in action — and find help on how you can turn this template into your own.

### Jump right in

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><h4><i class="fa-bolt">:bolt:</i></h4></td><td><strong>Quickstart</strong></td><td>Create your first site</td><td></td><td></td><td><a href="/universal-kit/getting-started/quickstart">Quickstart</a></td></tr><tr><td><h4><i class="fa-leaf">:leaf:</i></h4></td><td><strong>Editor basics</strong></td><td>Learn the basics of GitBook</td><td></td><td></td><td><a href="https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md">https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md</a></td></tr><tr><td><h4><i class="fa-globe-pointer">:globe-pointer:</i></h4></td><td><strong>Publish your docs</strong></td><td>Share your docs online</td><td></td><td></td><td><a href="/universal-kit/getting-started/publish-your-docs">Publish your docs</a></td></tr></tbody></table>


# Quickstart

<figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-hero.png" alt=""><figcaption></figcaption></figure>

Beautiful documentation starts with the content you create — and GitBook makes it easy to get started with any pre-existing content.

{% hint style="info" %}
Want to learn about writing content from scratch? Head to the [Basics](/universal-kit/basics/editor) section to learn more.
{% endhint %}

### Import

GitBook supports importing content from many popular writing tools and formats. If your content already exists, you can upload a file or group of files to be imported.

<div data-full-width="false"><figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-import.png" alt=""><figcaption></figcaption></figure></div>

### Sync a repository

GitBook also allows you to set up a bi-directional sync with an existing repository on GitHub or GitLab. Setting up Git Sync allows you and your team to write content in GitBook or in code, and never have to worry about your content becoming out of sync.


# Publish your docs

Once you’ve finished writing, editing, or importing your content, you can publish your work to the web as a docs site. Once published, your site will be accessible online only to your selected audience.

You can publish your site and find related settings from your docs site's homepage.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/publish-hero.png" alt=""><figcaption></figcaption></figure>


# Editor

GitBook has a powerful block-based editor that allows you to seamlessly create, update, and enhance your content.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/editor-hero.png" alt=""><figcaption></figcaption></figure>

### Writing content

GitBook offers a range of block types for you to add to your content inline — from simple text and tables, to code blocks and more. These elements will make your pages more useful to readers, and offer extra information and context.

Either start typing below, or press `/` to see a list of the blocks you can insert into your page.

### Add a new block

{% stepper %}
{% step %}

#### Open the insert block menu

Press `/` on your keyboard to open the insert block menu.
{% endstep %}

{% step %}

#### Search for the block you need

Try searching for “Stepper”, for exampe, to insert the stepper block.
{% endstep %}

{% step %}

#### Insert and edit your block

Click or press Enter to insert your block. From here, you’ll be able to edit it as needed.
{% endstep %}
{% endstepper %}


# Markdown

GitBook supports many different types of content, and is backed by Markdown — meaning you can copy and paste any existing Markdown files directly into the editor!

<figure><img src="https://gitbookio.github.io/onboarding-template-images/markdown-hero.png" alt=""><figcaption></figcaption></figure>

Feel free to test it out and copy the Markdown below by hovering over the code block in the upper right, and pasting into a new line underneath.

```markdown
# Heading

This is some paragraph text, with a [link](https://docs.gitbook.com) to our docs. 

## Heading 2
- Point 1
- Point 2
- Point 3
```

{% hint style="info" %}
If you have multiple files, GitBook makes it easy to import full repositories too — allowing you to keep your GitBook content in sync.
{% endhint %}


# Images & media

GitBook allows you to add images and media easily to your docs. Simply drag a file into the editor, or use the file manager in the upper right corner to upload multiple images at once.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/images-hero.png" alt=""><figcaption><p>Add alt text and captions to your images</p></figcaption></figure>

{% hint style="info" %}
You can also add images simply by copying and pasting them directly into the editor — and GitBook will automatically add it to your file manager.
{% endhint %}


# Interactive blocks

In addition to the default Markdown you can write, GitBook has a number of out-of-the-box interactive blocks you can use. You can find interactive blocks by pressing `/` from within the editor.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/interactive-hero.png" alt=""><figcaption></figcaption></figure>

### Tabs

{% tabs %}
{% tab title="First tab" %}
Each tab is like a mini page — it can contain multiple other blocks, of any type. So you can add code blocks, images, integration blocks and more to individual tabs in the same tab block.
{% endtab %}

{% tab title="Second tab" %}
Add images, embedded content, code blocks, and more.

```javascript
const handleFetchEvent = async (request, context) => {
    return new Response({message: "Hello World"});
};
```

{% endtab %}
{% endtabs %}

### Expandable sections

<details>

<summary>Click me to expand</summary>

Expandable blocks are helpful in condensing what could otherwise be a lengthy paragraph. They are also great in step-by-step guides and FAQs.

</details>

### Embedded content

{% embed url="<https://www.youtube.com/watch?v=YILlrDYzAm4>" %}

{% hint style="info" %}
GitBook supports thousands of embedded websites out-of-the-box, simply by pasting their links. Feel free to check out which ones[ are supported natively](https://iframely.com).
{% endhint %}


# Integrations

GitBook integrations allow you to connect your GitBook spaces to some of your favorite platforms and services. You can install integrations into your GitBook page from the *Integrations* menu in the top left.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/integrations-hero.png" alt=""><figcaption></figcaption></figure>

### Types of integrations

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th></tr></thead><tbody><tr><td><strong>Analytics</strong></td><td>Track analytics from your docs</td><td><a href="https://www.gitbook.com/integrations#analytics">https://www.gitbook.com/integrations#analytics</a></td><td></td><td></td></tr><tr><td><strong>Support</strong></td><td>Add support widgets to your docs</td><td><a href="https://www.gitbook.com/integrations#support">https://www.gitbook.com/integrations#support</a></td><td></td><td></td></tr><tr><td><strong>Interactive</strong></td><td>Add extra functionality to your docs</td><td><a href="https://www.gitbook.com/integrations#interactive">https://www.gitbook.com/integrations#interactive</a></td><td></td><td></td></tr><tr><td><strong>Visitor Authentication</strong></td><td>Protect your docs and require sign-in</td><td><a href="https://www.gitbook.com/integrations#visitor-authentication">https://www.gitbook.com/integrations#visitor-authentication</a></td><td></td><td></td></tr></tbody></table>


