diff --git a/.gitignore b/.gitignore
index 03b7188..da29450 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ frontend/node_modules/.vite/
.env
.env.*
!.env.example
+infra/local-prod/casan.local.env
backend/.env
frontend/.env
**/.env
diff --git a/.specify/logs/audit/security.jsonl b/.specify/logs/audit/security.jsonl
index ab92746..8234e22 100644
--- a/.specify/logs/audit/security.jsonl
+++ b/.specify/logs/audit/security.jsonl
@@ -82,3 +82,13 @@
{"timestamp":"2026-07-08T09:53:30Z","trace_id":"412d7566-4200-42a8-8772-b88bf395c1f1","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"427aa6ae245aa0f05cc75c0aea61de8129aae1e0e589d5727873d4d7f9baf872","output_hash":"427aa6ae245aa0f05cc75c0aea61de8129aae1e0e589d5727873d4d7f9baf872"}
{"timestamp":"2026-07-08T09:53:32Z","trace_id":"506f4e4f-88f0-4a76-9561-b93acd65b026","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"427aa6ae245aa0f05cc75c0aea61de8129aae1e0e589d5727873d4d7f9baf872","output_hash":"427aa6ae245aa0f05cc75c0aea61de8129aae1e0e589d5727873d4d7f9baf872"}
{"timestamp":"2026-07-08T09:53:33Z","trace_id":"4c96ed73-034e-4d15-b99b-12ef890372b0","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"427aa6ae245aa0f05cc75c0aea61de8129aae1e0e589d5727873d4d7f9baf872","output_hash":"427aa6ae245aa0f05cc75c0aea61de8129aae1e0e589d5727873d4d7f9baf872"}
+{"timestamp":"2026-07-10T06:11:24Z","trace_id":"trace-1783663884-37","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab","output_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab"}
+{"timestamp":"2026-07-10T06:12:28Z","trace_id":"trace-1783663948-37","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab","output_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab"}
+{"timestamp":"2026-07-10T06:13:42Z","trace_id":"trace-1783664022-37","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab","output_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab"}
+{"timestamp":"2026-07-10T06:14:39Z","trace_id":"trace-1783664079-613","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"e756b35ca2e4c3326cada62e0a6d4b3d714c5afb0caab2268c86c63207228ed2","output_hash":"e756b35ca2e4c3326cada62e0a6d4b3d714c5afb0caab2268c86c63207228ed2"}
+{"timestamp":"2026-07-10T06:18:36Z","trace_id":"trace-1783664316-37","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab","output_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab"}
+{"timestamp":"2026-07-10T06:19:34Z","trace_id":"trace-1783664374-636","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"142404dfd1bd2e31f6bef54bb495685e059a9b5c4b2e3369c8b79f8c7bec67a9","output_hash":"142404dfd1bd2e31f6bef54bb495685e059a9b5c4b2e3369c8b79f8c7bec67a9"}
+{"timestamp":"2026-07-10T06:19:37Z","trace_id":"trace-1783664377-747","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab","output_hash":"5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab"}
+{"timestamp":"2026-07-10T06:20:20Z","trace_id":"trace-1783664420-1347","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"d1c3c3a7ee70994d948443ba05bb0a9c2dd1f1852a994ab80d722730d282ae44","output_hash":"d1c3c3a7ee70994d948443ba05bb0a9c2dd1f1852a994ab80d722730d282ae44"}
+{"timestamp":"2026-07-10T07:20:30Z","trace_id":"trace-1783668030-52","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"8aaecd06d85c7f743d3545e7b865f3053c647591f95919c1c0b003046d4757aa","output_hash":"8aaecd06d85c7f743d3545e7b865f3053c647591f95919c1c0b003046d4757aa"}
+{"timestamp":"2026-07-10T07:21:21Z","trace_id":"trace-1783668081-716","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"280cadd0be725e6496a8614b12a98836bedb5c8a38fc6b2a8afe5f44b237158e","output_hash":"280cadd0be725e6496a8614b12a98836bedb5c8a38fc6b2a8afe5f44b237158e"}
diff --git a/.specify/logs/cost/metrics.jsonl b/.specify/logs/cost/metrics.jsonl
index 120324a..5be67a8 100644
--- a/.specify/logs/cost/metrics.jsonl
+++ b/.specify/logs/cost/metrics.jsonl
@@ -15,3 +15,11 @@
{"timestamp":"2026-07-08T09:28:30Z","trace_id":"7622fc43-9b84-4216-a704-96384d9b9375","harness":"H6-agentops","agent":"unknown-agent","step":"agent_step","status":"success","exit_code":0,"latency_ms":224,"retry_count":0,"input_tokens":3,"output_tokens":3,"total_tokens":6,"cost_estimate":1.2e-05,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":[],"input_hash":"7df5d60319af2916ff4d99c086c43b3989be3420fffff872e1077a650ca528f4","output_hash":"7df5d60319af2916ff4d99c086c43b3989be3420fffff872e1077a650ca528f4"}
{"timestamp":"2026-07-08T09:53:28Z","trace_id":"65ede58f-6400-49c1-b09e-e723035f7251","harness":"H6-agentops","agent":"unknown-agent","step":"agent_step","status":"success","exit_code":0,"latency_ms":219,"retry_count":0,"input_tokens":3,"output_tokens":3,"total_tokens":6,"cost_estimate":1.2e-05,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":[],"input_hash":"7df5d60319af2916ff4d99c086c43b3989be3420fffff872e1077a650ca528f4","output_hash":"7df5d60319af2916ff4d99c086c43b3989be3420fffff872e1077a650ca528f4"}
{"timestamp":"2026-07-08T09:53:32Z","trace_id":"06a26538-e7c3-42c2-a202-fc90cf24924d","harness":"H6-agentops","agent":"unknown-agent","step":"agent_step","status":"success","exit_code":0,"latency_ms":243,"retry_count":0,"input_tokens":3,"output_tokens":3,"total_tokens":6,"cost_estimate":1.2e-05,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":[],"input_hash":"7df5d60319af2916ff4d99c086c43b3989be3420fffff872e1077a650ca528f4","output_hash":"7df5d60319af2916ff4d99c086c43b3989be3420fffff872e1077a650ca528f4"}
+{"timestamp": "2026-07-10T05:42:31Z", "trace_id": "ce743020-b73a-474a-871f-c465fb6eb472", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "failed", "exit_code": 2, "latency_ms": 159, "retry_count": 0, "input_tokens": 5, "output_tokens": 5, "total_tokens": 10, "cost_estimate": 0.0, "cost_source": "readonly_word_count", "synthesis_mode": "deterministic", "hallucination_signals": 0, "alerts": [], "input_hash": "5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab", "output_hash": "4f16989d1b5e09312bbd78611adcdae1219a4b19ec40252a986369155f049f96"}
+{"timestamp": "2026-07-10T06:10:20Z", "trace_id": "96b4c003-ecd4-4f2e-b861-8d48f085d645", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "failed", "exit_code": 2, "latency_ms": 141, "retry_count": 0, "input_tokens": 5, "output_tokens": 5, "total_tokens": 10, "cost_estimate": 0.0, "cost_source": "readonly_word_count", "synthesis_mode": "deterministic", "hallucination_signals": 0, "alerts": [], "input_hash": "5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab", "output_hash": "4f16989d1b5e09312bbd78611adcdae1219a4b19ec40252a986369155f049f96"}
+{"timestamp": "2026-07-10T06:11:24Z", "trace_id": "c924568f-2816-4ce0-b718-645b0c42733e", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "failed", "exit_code": 2, "latency_ms": 219, "retry_count": 0, "input_tokens": 5, "output_tokens": 5, "total_tokens": 10, "cost_estimate": 0.0, "cost_source": "readonly_word_count", "synthesis_mode": "deterministic", "hallucination_signals": 0, "alerts": [], "input_hash": "5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab", "output_hash": "4f16989d1b5e09312bbd78611adcdae1219a4b19ec40252a986369155f049f96"}
+{"timestamp": "2026-07-10T06:12:28Z", "trace_id": "10331f30-ce27-47c6-bc5a-9f9841ff8415", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "failed", "exit_code": 2, "latency_ms": 342, "retry_count": 0, "input_tokens": 5, "output_tokens": 5, "total_tokens": 10, "cost_estimate": 0.0, "cost_source": "readonly_word_count", "synthesis_mode": "deterministic", "hallucination_signals": 0, "alerts": [], "input_hash": "5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab", "output_hash": "4f16989d1b5e09312bbd78611adcdae1219a4b19ec40252a986369155f049f96"}
+{"timestamp": "2026-07-10T06:14:39Z", "trace_id": "2e9b93cb-f3a7-4ceb-accd-4e8001562d0a", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "success", "exit_code": 0, "latency_ms": 57161, "retry_count": 0, "input_tokens": 5, "output_tokens": 52, "total_tokens": 57, "cost_estimate": 0.0, "cost_source": "readonly_word_count", "synthesis_mode": "deterministic", "hallucination_signals": 0, "alerts": [], "input_hash": "5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab", "output_hash": "e756b35ca2e4c3326cada62e0a6d4b3d714c5afb0caab2268c86c63207228ed2"}
+{"timestamp": "2026-07-10T06:19:34Z", "trace_id": "d8796e51-f1bf-4950-b27d-5327ed69ee12", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "success", "exit_code": 0, "latency_ms": 57864, "retry_count": 0, "input_tokens": 427, "output_tokens": 406, "total_tokens": 833, "cost_estimate": 0.0, "cost_source": "ollama_local_real_tokens", "synthesis_mode": "model", "hallucination_signals": 0, "alerts": [], "input_hash": "5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab", "output_hash": "142404dfd1bd2e31f6bef54bb495685e059a9b5c4b2e3369c8b79f8c7bec67a9"}
+{"timestamp": "2026-07-10T06:20:20Z", "trace_id": "b5ad414a-4662-49c5-a024-3f2bb51ada64", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "success", "exit_code": 0, "latency_ms": 43174, "retry_count": 0, "input_tokens": 511, "output_tokens": 288, "total_tokens": 799, "cost_estimate": 0.0, "cost_source": "ollama_local_real_tokens", "synthesis_mode": "model", "hallucination_signals": 0, "alerts": [], "input_hash": "5da5739dfab4b282139a20d7272d0e676331eb09f86f562cc5e6d1f3b668a5ab", "output_hash": "d1c3c3a7ee70994d948443ba05bb0a9c2dd1f1852a994ab80d722730d282ae44"}
+{"timestamp": "2026-07-10T07:21:22Z", "trace_id": "1fd5d347-f21c-4370-a60d-30b11fd8c53e", "harness": "H6-agentops", "agent": "chat.ask-casan", "step": "ask-casan-readonly", "status": "success", "exit_code": 0, "latency_ms": 51212, "retry_count": 0, "input_tokens": 362, "output_tokens": 59, "total_tokens": 421, "cost_estimate": 0.0, "cost_source": "ollama_local_real_tokens", "synthesis_mode": "model", "hallucination_signals": 0, "alerts": [], "input_hash": "8aaecd06d85c7f743d3545e7b865f3053c647591f95919c1c0b003046d4757aa", "output_hash": "280cadd0be725e6496a8614b12a98836bedb5c8a38fc6b2a8afe5f44b237158e"}
diff --git a/.specify/logs/level5/fallback.jsonl b/.specify/logs/level5/fallback.jsonl
index 6ddab32..b9d4615 100644
--- a/.specify/logs/level5/fallback.jsonl
+++ b/.specify/logs/level5/fallback.jsonl
@@ -1,2 +1,3 @@
{"timestamp":"2026-07-08T09:20:52Z","trace_id":"42b46ba8-3399-440e-aaee-da88dc06bc1c","harness":"L5-model-fallback","primary_exit":9,"route":"fallback","final_exit":0,"output":"/Users/thanhnguyen/Documents/AI/HarnessHkt/Harness_Hakathon/Output_CASAN5_REFINED/docs/output/casan/level5-evidence/10-fallback-output.txt"}
{"timestamp":"2026-07-08T09:21:54Z","trace_id":"bc29e2dd-0930-4ded-9cd3-a011b5d24c4d","harness":"L5-model-fallback","primary_exit":1,"route":"fallback","final_exit":0,"output":"/var/folders/zn/qn8sqwzn18g34ddftsxgyz6r0000gn/T/tmp.2HkWTSLEMD/fb.out"}
+{"timestamp":"2026-07-10T06:09:41Z","trace_id":"ed6b7bc1-3030-4656-9097-69ba16f47e5e","harness":"L5-model-fallback","primary_exit":2,"route":"fallback","final_exit":0,"output":"/var/folders/zn/qn8sqwzn18g34ddftsxgyz6r0000gn/T/tmp.jBxa5B3jDQ/fb.out"}
diff --git a/.specify/logs/level5/provider-usage.jsonl b/.specify/logs/level5/provider-usage.jsonl
index d3d0d28..0b56a8e 100644
--- a/.specify/logs/level5/provider-usage.jsonl
+++ b/.specify/logs/level5/provider-usage.jsonl
@@ -2,3 +2,14 @@
{"timestamp": "2026-07-08T09:20:56Z", "harness": "L5-provider-telemetry", "provider": "sample-provider", "model": "sample-model-large", "run_id": "provider-run-001", "step": "speckit.implement", "input_tokens": 1842, "output_tokens": 936, "total_tokens": 2778, "cost_usd": 0.08334, "latency_ms": 4210, "status": "success"}
{"timestamp": "2026-07-08T09:22:30Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "judge", "role": "judge", "input_tokens": 168, "output_tokens": 3, "total_tokens": 171, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 1515, "status": "success"}
{"timestamp": "2026-07-08T09:22:36Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "t4-telemetry-test", "role": "classify", "input_tokens": 208, "output_tokens": 2, "total_tokens": 210, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 1417, "status": "success"}
+{"timestamp": "2026-07-10T06:09:37Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 72, "output_tokens": 27, "total_tokens": 99, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 9972, "status": "success"}
+{"timestamp": "2026-07-10T06:09:40Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 218, "output_tokens": 3, "total_tokens": 221, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 2660, "status": "success"}
+{"timestamp": "2026-07-10T06:09:46Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 72, "output_tokens": 29, "total_tokens": 101, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 4034, "status": "success"}
+{"timestamp": "2026-07-10T06:13:44Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 2476, "status": "success"}
+{"timestamp": "2026-07-10T06:15:04Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 73, "output_tokens": 39, "total_tokens": 112, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 4967, "status": "success"}
+{"timestamp": "2026-07-10T06:18:39Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 2503, "status": "success"}
+{"timestamp": "2026-07-10T06:19:34Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 427, "output_tokens": 406, "total_tokens": 833, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 41620, "status": "success"}
+{"timestamp": "2026-07-10T06:19:38Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 626, "status": "success"}
+{"timestamp": "2026-07-10T06:20:20Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 511, "output_tokens": 288, "total_tokens": 799, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 31437, "status": "success"}
+{"timestamp": "2026-07-10T07:20:36Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 207, "output_tokens": 2, "total_tokens": 209, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 5868, "status": "success"}
+{"timestamp": "2026-07-10T07:21:21Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 362, "output_tokens": 59, "total_tokens": 421, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 4829, "status": "success"}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..0eed0a0
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,517 @@
+# Codex Instructions for "OKR Web Application" Project
+
+You are an expert full-stack developer specializing in NestJS and a **master of React with Vite and Tailwind CSS**. Your primary goal is to generate code that is secure, efficient, and **strictly follows this project's architecture and design documents**. Do not invent features or logic. Every piece of code must be traceable to a design specification.
+
+## 1. The Supreme Rule: Adhere to Architecture & Design
+
+**Before generating any code, you must understand the project's structure.**
+
+- **Architecture:** Refer to `docs/technical_architecture.md` for the overall system design, module responsibilities, and technology stack.
+
+- **Requirements:** Refer to `apps/okr/domain/input/okr-requirement.md` for functional requirements, use cases, and UI mockups.
+
+- **Your Task:** Your role is to translate these designs into code, not to be creative.
+
+**Example Check:**
+
+- **CORRECT:** Logic for key result progress updates belongs in the `key-results` module.
+
+- **INCORRECT:** Placing key result logic directly within the `objectives` service.
+
+- **Typescript Types:** Use TypeScript types and interfaces as defined in the design documents. Do not create new types unless explicitly required by the design.
+
+## 2. Key Technologies & Libraries to Prioritize
+
+⚠️ **ABSOLUTE LIBRARY RESTRICTION - ZERO TOLERANCE POLICY:**
+- **FORBIDDEN:** Installing ANY new libraries beyond those already listed in package.json
+- **MANDATORY:** Use ONLY existing libraries from architecture.md dependencies
+- **EXCEPTION PROCESS:** If absolutely critical to add a new library, must:
+ 1. Stop all code generation
+ 2. Ask explicit permission from user
+ 3. Provide detailed justification of purpose and necessity
+ 4. Explain why existing libraries cannot fulfill the requirement
+ 5. Wait for user approval before proceeding
+- **FOCUS:** Maximize capabilities of existing libraries rather than seeking new ones
+
+**Use only libraries from architecture.md, do not install new libraries.**
+
+### Frontend Stack (React + Vite SPA):
+
+| Library | Role |
+|---------|------|
+| **React 18.x** | UI framework |
+| **Vite 5.x** | Build tool & dev server with HMR |
+| **React Router DOM 6.x** | Client-side routing (``, ``, ``) |
+| **TanStack Query 5.x** | Server state management, caching (`useQuery`, `useMutation`) |
+| **Axios 1.x** | HTTP client — all API calls via `src/lib/api.ts` |
+| **React Hook Form 7.x** | Form state and submission |
+| **Zod 3.x** | Schema validation — schemas in `src/schemas/`, shared with backend DTOs |
+| **Tailwind CSS 3.x** | Utility-first styling — **only** styling tool, no CSS-in-JS |
+
+### Backend Stack (NestJS):
+
+| Library | Role |
+|---------|------|
+| **NestJS 10.x** | Application framework (modules, controllers, services, decorators) |
+| **Prisma 5.x** | ORM — `schema.prisma` is single source of truth, use Prisma client for all DB ops |
+| **@nestjs/jwt** | JWT sign/verify (no Passport — Username/Password only, no SSO) |
+| **bcrypt** | Password hashing (cost factor 12) |
+| **class-validator + class-transformer** | DTO validation with `ValidationPipe` |
+| **@nestjs/swagger** | OpenAPI/Swagger UI at `/api/docs` (dev only) |
+
+## 2.1. Design Style Guidelines
+
+**Design Style: Clean Modern Dashboard (Pure Tailwind CSS)**
+
+This project uses **pure Tailwind CSS** — no external component library (no MUI, no Ant Design). The design is clean, professional, and matches the OKR dashboard wireframes defined in `apps/okr/domain/input/okr-requirement.md`.
+
+### Layout Structure (matches OKR wireframes):
+
+```
++------------------+----------------------------------------------+
+| Sidebar (fixed) | Top Header (fixed) |
+| - Year nav +----------------------------------------------+
+| - My OKRs | Main Content Area (scrollable) |
+| - Members | |
+| - OKR - all | |
++------------------+----------------------------------------------+
+```
+
+### Color Palette:
+
+| Color Name | Tailwind Class | Usage |
+|------------|----------------|-------|
+| **Background** | `bg-gray-50` | Page background |
+| **White** | `bg-white` | Cards, sidebar, content areas |
+| **Primary Text** | `text-gray-800` | Titles, important content |
+| **Secondary Text** | `text-gray-500` | Labels, descriptions, metadata |
+| **Primary Blue** | `bg-blue-600` / `text-blue-600` | Primary actions, active nav links |
+| **Success Green** | `text-green-600` / `bg-green-100` | Success status (100% / Completed) |
+| **Warning Orange** | `text-orange-500` / `bg-orange-100` | In-progress / pending status |
+| **Border** | `border-gray-200` | Card borders, dividers |
+| **Sidebar** | `bg-white border-r border-gray-200` | Left navigation panel |
+
+### Component Style Rules:
+
+1. **No CSS-in-JS** — no `sx` prop, no `styled()` — all styling via Tailwind utility classes.
+2. **Cards:** `bg-white rounded-xl shadow-sm border border-gray-200 p-6`
+3. **Primary button:** `bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors`
+4. **Secondary button:** `border border-gray-300 text-gray-700 hover:bg-gray-50 px-4 py-2 rounded-lg transition-colors`
+5. **Input fields:** `w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`
+6. **Select/Dropdown:** `border border-gray-300 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-blue-500`
+7. **Status badges:**
+ - Not Started: `bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs font-medium`
+ - In Progress: `bg-orange-100 text-orange-700 px-2 py-1 rounded-full text-xs font-medium`
+ - Completed: `bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs font-medium`
+8. **Progress bar:**
+ ```tsx
+
+ ```
+9. **Sidebar nav item (active):** `bg-blue-50 text-blue-600 font-medium`
+10. **Sidebar nav item (inactive):** `text-gray-600 hover:bg-gray-50 hover:text-gray-800`
+
+### Consistent Visual Rules:
+
+- All interactive elements must have hover effects (`hover:shadow-md`, `hover:-translate-y-px`, `transition-colors`).
+- Use consistent spacing: multiples of 4px (`p-4`, `gap-4`, `mt-6`).
+- Text must always use `gray` (not `grey`) — `text-gray-700`, `border-gray-200`.
+
+## 2.2. Tailwind CSS Configuration Rules (Tailwind v3)
+
+⚠️ **TAILWIND CSS v3 SPECIFIC REQUIREMENTS:**
+
+### CSS Import Rules (v3):
+```css
+✅ CORRECT:
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+❌ WRONG: @import "tailwindcss";
+❌ WRONG: @import "tailwindcss/base";
+```
+
+### Configuration File:
+```javascript
+// tailwind.config.ts
+import type { Config } from 'tailwindcss';
+
+const config: Config = {
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
+
+export default config;
+```
+
+### PostCSS Config:
+```javascript
+// postcss.config.js
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
+```
+
+### Color Class Rules:
+```css
+✅ CORRECT: text-gray-700, bg-gray-50, border-gray-200
+❌ WRONG: text-grey-700, bg-grey-50, border-grey-200
+```
+
+### Layout Rules:
+```tsx
+✅ CORRECT: Use semantic HTML + Tailwind flex/grid
+
+
Title
+
+
+
+❌ WRONG: Use third-party layout components
+
+ Title
+
+```
+
+### Hover State Rules:
+```css
+✅ CORRECT: hover:bg-gray-50, hover:bg-gray-100
+❌ WRONG: hover:bg-gray-25 (doesn't exist in Tailwind)
+```
+
+### Custom Styles Rules:
+```css
+✅ CORRECT: Use standard CSS in index.css (no @apply with @layer)
+.okr-card {
+ background: white;
+ border-radius: 0.75rem;
+ border: 1px solid #e5e7eb;
+}
+
+❌ WRONG: Use @apply with @layer
+@layer components {
+ .okr-card { @apply bg-white rounded-xl border border-gray-200; }
+}
+```
+
+## 3. Backend Generation Rules (NestJS + Prisma)
+
+### Controller Rules:
+
+- **Location:** Must be in `backend/src/[module-name]/[module-name].controller.ts`.
+- **Responsibility:** Keep controllers "thin". They only receive requests, trigger guards, validate DTOs, and call a single service method.
+- **Auth:** Use `@UseGuards(JwtAuthGuard)` and `@Roles()` decorator on all protected routes.
+
+### Service Rules:
+
+- **Location:** Must be in `backend/src/[module-name]/[module-name].service.ts`.
+- **Responsibility:** All business logic lives here.
+- **Key Logic:**
+ - Use Prisma client for all DB operations — **no raw SQL** in application code.
+ - Filter data by role: ADMIN sees all, MANAGER sees all, EMPLOYEE sees only own objectives.
+ - Throw specific NestJS exceptions (`NotFoundException`, `ForbiddenException`).
+
+### Prisma Schema Rules:
+
+- **Single source of truth:** `backend/prisma/schema.prisma` defines ALL tables.
+- **Migrations:** Use `npx prisma migrate dev --name ` — never edit migration files manually.
+- **Types:** Always use Prisma-generated types (`Prisma.ObjectiveCreateInput`, `Prisma.KeyResultUpdateInput`).
+
+### Module Structure (OKR Domain):
+
+```
+backend/src/
+├── auth/ # JWT login, refresh token endpoints
+├── users/ # User CRUD (Admin/Manager only)
+├── objectives/ # Objective CRUD, filtering by quarter/owner/status
+├── key-results/ # KR CRUD, progress update (PATCH /:id/progress)
+└── common/ # Guards, filters, interceptors, decorators
+```
+
+### Database Seed Management:
+
+- **CRITICAL:** After completing backend code with schema changes, **ALWAYS** update the seed file.
+- **Seed file location:** `backend/prisma/seed.ts`
+- **Execution:** `npx prisma db seed` (or automatically on container start — always seeded in workshop environment)
+- **Idempotency:** Use Prisma `upsert` keyed on stable identifiers — running seed twice must produce no duplicates.
+
+```typescript
+// backend/prisma/seed.ts
+import { PrismaClient } from '@prisma/client';
+import * as bcrypt from 'bcrypt';
+
+const prisma = new PrismaClient();
+
+async function main() {
+ // Always seed — workshop environment, no env gate needed
+
+ const passwordHash = await bcrypt.hash('Password@123', 10);
+
+ // Users — upsert keyed on email
+ const admin = await prisma.user.upsert({
+ where: { email: 'admin@okr.local' },
+ update: {},
+ create: { name: 'System Admin', email: 'admin@okr.local', password: passwordHash, role: 'ADMIN' },
+ });
+
+ const manager = await prisma.user.upsert({
+ where: { email: 'manager@okr.local' },
+ update: {},
+ create: { name: 'Nguyen Van Manager', email: 'manager@okr.local', password: passwordHash, role: 'MANAGER' },
+ });
+
+ const employee = await prisma.user.upsert({
+ where: { email: 'employee@okr.local' },
+ update: {},
+ create: { name: 'Nguyen Van A', email: 'employee@okr.local', password: passwordHash, role: 'EMPLOYEE' },
+ });
+
+ // Objectives — upsert keyed on id
+ const obj1 = await prisma.objective.upsert({
+ where: { id: 1 },
+ update: {},
+ create: {
+ title: 'POC AI for SQL Injection prevention',
+ description: 'Evaluate AI tools for automated SQL injection detection',
+ ownerId: employee.id,
+ quarter: 'Q2/2026',
+ status: 'IN_PROGRESS',
+ },
+ });
+
+ // Key Results — upsert keyed on id
+ await prisma.keyResult.upsert({
+ where: { id: 1 },
+ update: {},
+ create: {
+ objectiveId: obj1.id,
+ title: 'Complete 3 POC sessions with security team',
+ progress: 0,
+ startValue: 0,
+ targetValue: 3,
+ deadline: new Date('2026-06-30'),
+ },
+ });
+
+ console.log('Seed completed successfully.');
+}
+
+main()
+ .catch((e) => { console.error(e); process.exit(1); })
+ .finally(() => prisma.$disconnect());
+```
+
+### Seed Data Requirements (OKR Domain):
+
+| Category | Records | Notes |
+|----------|---------|-------|
+| Users | ≥ 3 | Admin, Manager, Employee — password `Password@123` hashed with bcrypt |
+| Objectives | ≥ 2 | One per owner, varied statuses (`NOT_STARTED`, `IN_PROGRESS`) |
+| Key Results | ≥ 4 | Linked to objectives, with `progress`, `startValue`, `targetValue`, `deadline` |
+
+- **When to update seed:** After adding new Prisma model fields, after changing relations, after completing any backend module.
+- **Run seed:** `docker-compose exec backend npx prisma db seed`
+
+## 4. Frontend Generation Rules (React + Vite)
+
+### Routing Rules (React Router DOM v6):
+
+```tsx
+// frontend/src/App.tsx — route structure
+
+ } />
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+
+
+
+```
+
+### Component & File Location Rules:
+
+- **Route-level pages:** `frontend/src/pages/` (Login, Dashboard, OKRDetail, CreateObjective, KeyResultDetail)
+- **Layout components:** `frontend/src/components/layout/` (Sidebar, Header, AppLayout)
+- **Reusable UI components:** `frontend/src/components/ui/` (Button, ProgressBar, Badge, Table)
+- **Custom hooks:** `frontend/src/hooks/` (useAuth, useObjectives, useKeyResults)
+- **API client:** `frontend/src/lib/api.ts` — all Axios calls go here
+- **Query client config:** `frontend/src/lib/queryClient.ts`
+- **Zod schemas:** `frontend/src/schemas/`
+- **TypeScript interfaces:** `frontend/src/types/`
+
+### Layout Construction Rules:
+
+- **ONLY** use semantic HTML elements (``, `