From 840a89ea0ac9c62c4e916540585b6ffee51e2a27 Mon Sep 17 00:00:00 2001 From: amal Date: Wed, 15 Apr 2026 08:45:04 +0800 Subject: [PATCH 1/4] upd: qwen --- .../console-2026-04-14T09-51-59-546Z.log | 17 +++ .../console-2026-04-14T09-52-33-736Z.log | 17 +++ .../console-2026-04-14T09-53-25-466Z.log | 20 +++ .../console-2026-04-15T00-10-32-890Z.log | 18 +++ .../console-2026-04-15T00-12-16-354Z.log | 18 +++ .../page-2026-04-14T09-52-00-449Z.yml | 21 +++ .../page-2026-04-14T09-52-34-199Z.yml | 4 + .../page-2026-04-14T09-53-25-737Z.yml | 39 ++++++ .../page-2026-04-14T09-54-11-031Z.yml | 40 ++++++ .../page-2026-04-15T00-10-33-255Z.yml | 4 + .../page-2026-04-15T00-12-16-729Z.yml | 131 ++++++++++++++++++ .qwen/settings.json | 13 ++ .qwen/settings.json.orig | 9 ++ CLAUDE.md | 9 ++ PLAYWRIGHT_MCP.md | 100 +++++++++++++ bun.lockb | Bin 157810 -> 160613 bytes login-snapshot.yml | 35 +++++ package.json | 6 +- playwright-report/index.html | 90 ++++++++++++ playwright.config.ts | 27 ++++ test-results/.last-run.json | 4 + 21 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 .playwright-mcp/console-2026-04-14T09-51-59-546Z.log create mode 100644 .playwright-mcp/console-2026-04-14T09-52-33-736Z.log create mode 100644 .playwright-mcp/console-2026-04-14T09-53-25-466Z.log create mode 100644 .playwright-mcp/console-2026-04-15T00-10-32-890Z.log create mode 100644 .playwright-mcp/console-2026-04-15T00-12-16-354Z.log create mode 100644 .playwright-mcp/page-2026-04-14T09-52-00-449Z.yml create mode 100644 .playwright-mcp/page-2026-04-14T09-52-34-199Z.yml create mode 100644 .playwright-mcp/page-2026-04-14T09-53-25-737Z.yml create mode 100644 .playwright-mcp/page-2026-04-14T09-54-11-031Z.yml create mode 100644 .playwright-mcp/page-2026-04-15T00-10-33-255Z.yml create mode 100644 .playwright-mcp/page-2026-04-15T00-12-16-729Z.yml create mode 100644 .qwen/settings.json create mode 100644 .qwen/settings.json.orig create mode 100644 PLAYWRIGHT_MCP.md create mode 100644 login-snapshot.yml create mode 100644 playwright-report/index.html create mode 100644 playwright.config.ts create mode 100644 test-results/.last-run.json diff --git a/.playwright-mcp/console-2026-04-14T09-51-59-546Z.log b/.playwright-mcp/console-2026-04-14T09-51-59-546Z.log new file mode 100644 index 0000000..f771fd2 --- /dev/null +++ b/.playwright-mcp/console-2026-04-14T09-51-59-546Z.log @@ -0,0 +1,17 @@ +[ 665ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:14336 +[ 708ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 709ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 715ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 716ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 716ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 716ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 716ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 716ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 diff --git a/.playwright-mcp/console-2026-04-14T09-52-33-736Z.log b/.playwright-mcp/console-2026-04-14T09-52-33-736Z.log new file mode 100644 index 0000000..842b82c --- /dev/null +++ b/.playwright-mcp/console-2026-04-14T09-52-33-736Z.log @@ -0,0 +1,17 @@ +[ 358ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:14336 +[ 375ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 375ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 379ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 379ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 379ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 379ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 380ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 diff --git a/.playwright-mcp/console-2026-04-14T09-53-25-466Z.log b/.playwright-mcp/console-2026-04-14T09-53-25-466Z.log new file mode 100644 index 0000000..669e57e --- /dev/null +++ b/.playwright-mcp/console-2026-04-14T09-53-25-466Z.log @@ -0,0 +1,20 @@ +[ 137ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:14336 +[ 143ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 143ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 145ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 146ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 146ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 175ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/api/auth/session:0 +[ 43606ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/api/auth/login:0 +[ 77901ms] [ERROR] Unsupported style property %s. Did you mean %s? &[data-active] &[dataActive] @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:1804 diff --git a/.playwright-mcp/console-2026-04-15T00-10-32-890Z.log b/.playwright-mcp/console-2026-04-15T00-10-32-890Z.log new file mode 100644 index 0000000..f1bb14a --- /dev/null +++ b/.playwright-mcp/console-2026-04-15T00-10-32-890Z.log @@ -0,0 +1,18 @@ +[ 240ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:14336 +[ 265ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 265ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 272ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 273ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 273ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 428ms] [ERROR] Unsupported style property %s. Did you mean %s? &[data-active] &[dataActive] @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:1804 diff --git a/.playwright-mcp/console-2026-04-15T00-12-16-354Z.log b/.playwright-mcp/console-2026-04-15T00-12-16-354Z.log new file mode 100644 index 0000000..b48232e --- /dev/null +++ b/.playwright-mcp/console-2026-04-15T00-12-16-354Z.log @@ -0,0 +1,18 @@ +[ 193ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:14336 +[ 216ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 216ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 222ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 222ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 223ms] [ERROR] forwardRef render functions accept exactly two parameters: props and ref. %s Did you forget to use the ref parameter? @ http://localhost:3000/node_modules/.vite/deps/react-B6J-hxuQ.js?v=bf7d8134:644 +[ 279ms] [ERROR] Unsupported style property %s. Did you mean %s? &[data-active] &[dataActive] @ http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=bf7d8134:1804 diff --git a/.playwright-mcp/page-2026-04-14T09-52-00-449Z.yml b/.playwright-mcp/page-2026-04-14T09-52-00-449Z.yml new file mode 100644 index 0000000..134745c --- /dev/null +++ b/.playwright-mcp/page-2026-04-14T09-52-00-449Z.yml @@ -0,0 +1,21 @@ +- generic [active] [ref=e1]: + - generic: + - generic: + - generic: Loading... + - generic [ref=e4]: + - generic [ref=e5]: + - img [ref=e6] + - img [ref=e8] + - heading "Bun + Elysia + Vite + React" [level=1] [ref=e16] + - paragraph [ref=e17]: Full-stack starter template with Mantine UI, TanStack Router, and session-based auth. + - generic [ref=e18]: + - link "Login" [ref=e19] [cursor=pointer]: + - /url: /login + - generic [ref=e20]: + - img [ref=e22] + - generic [ref=e26]: Login + - link "Dashboard" [ref=e27] [cursor=pointer]: + - /url: /dashboard + - generic [ref=e28]: + - img [ref=e30] + - generic [ref=e34]: Dashboard \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-14T09-52-34-199Z.yml b/.playwright-mcp/page-2026-04-14T09-52-34-199Z.yml new file mode 100644 index 0000000..3636b06 --- /dev/null +++ b/.playwright-mcp/page-2026-04-14T09-52-34-199Z.yml @@ -0,0 +1,4 @@ +- generic [active]: + - generic: + - generic: + - generic: Loading... \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-14T09-53-25-737Z.yml b/.playwright-mcp/page-2026-04-14T09-53-25-737Z.yml new file mode 100644 index 0000000..4df40d9 --- /dev/null +++ b/.playwright-mcp/page-2026-04-14T09-53-25-737Z.yml @@ -0,0 +1,39 @@ +- generic [active] [ref=e1]: + - generic: + - generic: + - generic: Loading... + - generic [ref=e6]: + - heading "Login" [level=2] [ref=e7] + - paragraph [ref=e8]: + - text: "Demo:" + - strong [ref=e9]: superadmin@example.com + - text: / + - strong [ref=e10]: superadmin123 + - text: "or:" + - strong [ref=e11]: user@example.com + - text: / + - strong [ref=e12]: user123 + - generic [ref=e13]: + - generic [ref=e14]: Email * + - generic [ref=e15]: + - img [ref=e17] + - textbox "Email" [ref=e20]: + - /placeholder: email@example.com + - generic [ref=e21]: + - generic [ref=e22]: Password * + - generic [ref=e23]: + - img [ref=e25] + - textbox "Password" [ref=e30] + - button [ref=e32] [cursor=pointer]: + - img [ref=e34] + - button "Sign in" [ref=e36] [cursor=pointer]: + - generic [ref=e37]: + - img [ref=e39] + - generic [ref=e43]: Sign in + - separator [ref=e44]: + - generic [ref=e45]: or + - link "Login with Google" [ref=e46] [cursor=pointer]: + - /url: /api/auth/google + - generic [ref=e47]: + - img [ref=e49] + - generic [ref=e54]: Login with Google \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-14T09-54-11-031Z.yml b/.playwright-mcp/page-2026-04-14T09-54-11-031Z.yml new file mode 100644 index 0000000..ced0efe --- /dev/null +++ b/.playwright-mcp/page-2026-04-14T09-54-11-031Z.yml @@ -0,0 +1,40 @@ +- generic [ref=e6]: + - heading "Login" [level=2] [ref=e7] + - paragraph [ref=e8]: + - text: "Demo:" + - strong [ref=e9]: superadmin@example.com + - text: / + - strong [ref=e10]: superadmin123 + - text: "or:" + - strong [ref=e11]: user@example.com + - text: / + - strong [ref=e12]: user123 + - alert [ref=e55]: + - generic [ref=e56]: + - img [ref=e58] + - generic [ref=e61]: Email atau password salah + - generic [ref=e13]: + - generic [ref=e14]: Email * + - generic [ref=e15]: + - img [ref=e17] + - textbox "Email" [ref=e20]: + - /placeholder: email@example.com + - text: superadmin@example.com + - generic [ref=e21]: + - generic [ref=e22]: Password * + - generic [ref=e23]: + - img [ref=e25] + - textbox "Password" [ref=e30]: superadmin123 + - button [ref=e32] [cursor=pointer]: + - img [ref=e34] + - button "Sign in" [ref=e36] [cursor=pointer]: + - generic [ref=e37]: + - img [ref=e39] + - generic [ref=e43]: Sign in + - separator [ref=e44]: + - generic [ref=e45]: or + - link "Login with Google" [ref=e46] [cursor=pointer]: + - /url: /api/auth/google + - generic [ref=e47]: + - img [ref=e49] + - generic [ref=e54]: Login with Google \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-15T00-10-33-255Z.yml b/.playwright-mcp/page-2026-04-15T00-10-33-255Z.yml new file mode 100644 index 0000000..3636b06 --- /dev/null +++ b/.playwright-mcp/page-2026-04-15T00-10-33-255Z.yml @@ -0,0 +1,4 @@ +- generic [active]: + - generic: + - generic: + - generic: Loading... \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-15T00-12-16-729Z.yml b/.playwright-mcp/page-2026-04-15T00-12-16-729Z.yml new file mode 100644 index 0000000..1e0331d --- /dev/null +++ b/.playwright-mcp/page-2026-04-15T00-12-16-729Z.yml @@ -0,0 +1,131 @@ +- generic [active] [ref=e1]: + - generic: + - generic: + - generic: Loading... + - generic [ref=e3]: + - banner [ref=e4]: + - generic [ref=e5]: + - generic [ref=e6]: + - button [ref=e7] [cursor=pointer] + - generic [ref=e9]: + - img [ref=e11] + - paragraph [ref=e13]: Monitoring System + - generic [ref=e14]: + - button "Toggle color scheme" [ref=e15] [cursor=pointer]: + - img [ref=e17] + - generic "User" [ref=e20] [cursor=pointer]: + - img [ref=e21] + - navigation [ref=e23]: + - generic [ref=e24]: + - link "Dashboard" [ref=e25] [cursor=pointer]: + - /url: /dashboard + - img [ref=e27] + - generic [ref=e31]: Dashboard + - img [ref=e33] + - link "Applications" [ref=e35] [cursor=pointer]: + - /url: /apps + - img [ref=e37] + - generic [ref=e41]: Applications + - img [ref=e43] + - link "Log Activity" [ref=e45] [cursor=pointer]: + - /url: /logs + - img [ref=e47] + - generic [ref=e50]: Log Activity + - img [ref=e52] + - link "Error Reports" [ref=e54] [cursor=pointer]: + - /url: /bug-reports + - img [ref=e56] + - generic [ref=e58]: Error Reports + - img [ref=e60] + - link "Users" [ref=e62] [cursor=pointer]: + - /url: /users + - img [ref=e64] + - generic [ref=e67]: Users + - img [ref=e69] + - generic [ref=e72]: + - generic [ref=e73]: + - paragraph [ref=e74]: SYSTEM STATUS + - paragraph [ref=e77]: All Systems Operational + - button "Log out" [ref=e78] [cursor=pointer]: + - generic [ref=e79]: + - img [ref=e81] + - generic [ref=e85]: Log out + - main [ref=e86]: + - generic [ref=e88]: + - generic [ref=e90]: + - heading "Overview Dashboard" [level=2] [ref=e91] + - paragraph [ref=e92]: Welcome back, Super Admin. Here is what's happening today. + - generic [ref=e93]: + - generic [ref=e94]: + - img [ref=e97] + - generic [ref=e101]: + - paragraph [ref=e102]: Total Applications + - paragraph [ref=e103]: "1" + - generic [ref=e104]: + - img [ref=e107] + - generic [ref=e109]: + - paragraph [ref=e110]: New Errors + - paragraph [ref=e111]: "1" + - generic [ref=e112]: + - img [ref=e115] + - generic [ref=e120]: + - paragraph [ref=e121]: Users + - paragraph [ref=e122]: "4" + - generic [ref=e123]: + - heading "Registered Applications" [level=3] [ref=e124] + - link "View All Apps" [ref=e125] [cursor=pointer]: + - /url: /apps + - generic [ref=e126]: + - generic [ref=e127]: View All Apps + - img [ref=e129] + - generic [ref=e132]: + - generic [ref=e133]: + - generic [ref=e134]: + - img [ref=e137] + - generic [ref=e139]: + - paragraph [ref=e140]: Desa+ + - paragraph [ref=e141]: VERSION 2.4.1 + - generic [ref=e143]: ACTIVE + - link "View" [ref=e144] [cursor=pointer]: + - /url: /apps/desa-plus + - generic [ref=e145]: + - generic [ref=e146]: View + - img [ref=e148] + - generic [ref=e150]: + - heading "Recent Error Reports" [level=3] [ref=e151] + - link "View All Errors" [ref=e152] [cursor=pointer]: + - /url: /bug-reports + - generic [ref=e153]: + - generic [ref=e154]: View All Errors + - img [ref=e156] + - table [ref=e159]: + - rowgroup [ref=e160]: + - row "Application Error Message Version Time Severity" [ref=e161]: + - columnheader "Application" [ref=e162] + - columnheader "Error Message" [ref=e163] + - columnheader "Version" [ref=e164] + - columnheader "Time" [ref=e165] + - columnheader "Severity" [ref=e166] + - rowgroup [ref=e167]: + - row "desa-plus error saat menambah data project v2.1 1 days ago ON_HOLD" [ref=e168]: + - cell "desa-plus" [ref=e169]: + - paragraph [ref=e170]: desa-plus + - cell "error saat menambah data project" [ref=e171]: + - paragraph [ref=e172]: error saat menambah data project + - cell "v2.1" [ref=e173]: + - generic [ref=e175]: v2.1 + - cell "1 days ago" [ref=e176]: + - paragraph [ref=e177]: 1 days ago + - cell "ON_HOLD" [ref=e178]: + - generic [ref=e180]: ON_HOLD + - row "desa-plus error pada saat login v2.1.0 1 days ago OPEN" [ref=e181]: + - cell "desa-plus" [ref=e182]: + - paragraph [ref=e183]: desa-plus + - cell "error pada saat login" [ref=e184]: + - paragraph [ref=e185]: error pada saat login + - cell "v2.1.0" [ref=e186]: + - generic [ref=e188]: v2.1.0 + - cell "1 days ago" [ref=e189]: + - paragraph [ref=e190]: 1 days ago + - cell "OPEN" [ref=e191]: + - generic [ref=e193]: OPEN \ No newline at end of file diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 0000000..4e3ca7d --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--headless" + ], + "timeout": 30000 + } + }, + "$version": 3 +} \ No newline at end of file diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig new file mode 100644 index 0000000..5747629 --- /dev/null +++ b/.qwen/settings.json.orig @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest", "--headless"], + "timeout": 30000 + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 4063a63..af4f5fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,15 @@ React 19 + Vite 8 (middleware mode in dev). File-based routing with TanStack Rou - HMR: Vite 8 with `@vitejs/plugin-react` v6. `dedupeRefreshPlugin` fixes double React Refresh injection. - Editor: `REACT_EDITOR` env var. `zed` and `subl` use `file:line:col`, others use `--goto file:line:col`. +## Playwright MCP + +Playwright MCP server enables AI-assisted browser automation for testing and debugging. + +- MCP config: `.qwen/settings.json` — Qwen Code auto-loads on session start +- Playwright config: `playwright.config.ts` — E2E test configuration +- Run manually: `bun run mcp:playwright` — starts headless browser MCP server +- Install browsers: `bunx playwright install` — downloads Chromium and other browsers + ## Testing Tests use `bun:test`. Three levels: diff --git a/PLAYWRIGHT_MCP.md b/PLAYWRIGHT_MCP.md new file mode 100644 index 0000000..71f2df4 --- /dev/null +++ b/PLAYWRIGHT_MCP.md @@ -0,0 +1,100 @@ +# Playwright MCP Setup + +This project includes Playwright MCP (Model Context Protocol) for AI-assisted browser automation. + +## What is Playwright MCP? + +Playwright MCP allows AI assistants (like Claude) to interact with a real browser through the Model Context Protocol. This enables: +- Automated browser testing +- Web scraping and data extraction +- Visual testing and screenshots +- Navigation and interaction with web pages + +## Setup + +All dependencies are already installed: +- `@playwright/mcp` - MCP server for Playwright +- `@playwright/test` - Playwright test framework +- `playwright` - Browser automation library +- Chromium browser (downloaded via `bunx playwright install`) + +## Configuration + +### Qwen Code MCP Config (`.qwen/settings.json`) + +Qwen Code automatically loads this file on new session start: +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest", "--headless"], + "timeout": 30000 + } + } +} +``` + +### Playwright Config (`playwright.config.ts`) +Standard E2E test configuration with: +- Chromium browser +- Base URL: http://localhost:3000 +- Auto-starts dev server for testing + +## Usage + +### Start MCP Server +```bash +bun run mcp:playwright +``` + +This starts the MCP server on port 3000 in headless mode. AI assistants can connect to this server to control the browser. + +### Run E2E Tests +```bash +# Using Playwright's test runner +bunx playwright test + +# Using the existing test suite +bun run test:e2e +``` + +### Install/Update Browsers +```bash +# Install all browsers +bunx playwright install + +# Install specific browser +bunx playwright install chromium +``` + +## Integration with AI Assistants + +When using an AI assistant that supports MCP: +1. Start your app: `bun run dev` +2. Start the MCP server: `bun run mcp:playwright` +3. The AI assistant can now: + - Navigate to your app + - Take screenshots + - Click elements and fill forms + - Test user flows + - Debug UI issues + +## Available MCP Tools + +The Playwright MCP server provides tools for: +- `browser_navigate` - Navigate to a URL +- `browser_screenshot` - Take a screenshot +- `browser_click` - Click an element +- `browser_type` - Type text into an element +- `browser_select_option` - Select dropdown options +- `browser_hover` - Hover over elements +- `browser_evaluate` - Execute JavaScript +- `browser_snapshot` - Get page accessibility snapshot +- And more... + +## Files + +- `mcp.json` - MCP server configuration +- `playwright.config.ts` - Playwright test configuration +- `tests/e2e/` - E2E test files diff --git a/bun.lockb b/bun.lockb index a4112feec11f489ae524f6b8c5218848f1c5331b..c126c490de76b71b2828e543b93283a87c31d938 100755 GIT binary patch delta 26049 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 diff --git a/login-snapshot.yml b/login-snapshot.yml new file mode 100644 index 0000000..7b848e1 --- /dev/null +++ b/login-snapshot.yml @@ -0,0 +1,35 @@ +- generic [ref=e6]: + - heading "Login" [level=2] [ref=e7] + - paragraph [ref=e8]: + - text: "Demo:" + - strong [ref=e9]: superadmin@example.com + - text: / + - strong [ref=e10]: superadmin123 + - text: "or:" + - strong [ref=e11]: user@example.com + - text: / + - strong [ref=e12]: user123 + - generic [ref=e13]: + - generic [ref=e14]: Email * + - generic [ref=e15]: + - img [ref=e17] + - textbox "Email" [ref=e20]: + - /placeholder: email@example.com + - generic [ref=e21]: + - generic [ref=e22]: Password * + - generic [ref=e23]: + - img [ref=e25] + - textbox "Password" [ref=e30] + - button [ref=e32] [cursor=pointer]: + - img [ref=e34] + - button "Sign in" [ref=e36] [cursor=pointer]: + - generic [ref=e37]: + - img [ref=e39] + - generic [ref=e43]: Sign in + - separator [ref=e44]: + - generic [ref=e45]: or + - link "Login with Google" [ref=e46] [cursor=pointer]: + - /url: /api/auth/google + - generic [ref=e47]: + - img [ref=e49] + - generic [ref=e54]: Login with Google \ No newline at end of file diff --git a/package.json b/package.json index d0c4921..8dc37ea 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "db:seed": "bun run prisma/seed.ts", "db:studio": "bunx prisma studio", "db:generate": "bunx prisma generate", - "db:push": "bunx prisma db push" + "db:push": "bunx prisma db push", + "mcp:playwright": "playwright-mcp --headless --port 3000" }, "dependencies": { "@elysiajs/cors": "^1.4.1", @@ -43,11 +44,14 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.10", + "@playwright/mcp": "^0.0.70", + "@playwright/test": "^1.59.1", "@tanstack/router-vite-plugin": "^1.166.27", "@types/bun": "latest", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^6.0.1", + "playwright": "^1.59.1", "prisma": "6", "puppeteer-core": "^24.40.0", "typescript": "^6.0.2", diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..d8b3907 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,90 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d24867d --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'bun run dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file From c66ce4a39bc97c3a212129740d73321b9787fb99 Mon Sep 17 00:00:00 2001 From: amal Date: Wed, 15 Apr 2026 11:17:04 +0800 Subject: [PATCH 2/4] upd: auth Deskripsi: -update login - update struktur database No Issues --- .../migration.sql | 40 ++++ .../migration.sql | 3 + prisma/schema.prisma | 22 ++- prisma/seed.ts | 29 ++- src/app.ts | 171 ++++++++---------- src/frontend/components/AppCard.tsx | 15 +- src/frontend/components/DashboardLayout.tsx | 95 ++++++++-- src/frontend/hooks/useAuth.ts | 9 +- src/frontend/routes/apps.index.tsx | 15 +- src/frontend/routes/dashboard.tsx | 1 - src/frontend/routes/login.tsx | 24 +-- src/frontend/routes/profile.tsx | 3 +- src/frontend/routes/users.tsx | 17 +- tests/helpers.ts | 2 +- 14 files changed, 273 insertions(+), 173 deletions(-) create mode 100644 prisma/migrations/20260415014750_tambah_table_app_and_enum_role/migration.sql create mode 100644 prisma/migrations/20260415024317_update_enum_app/migration.sql diff --git a/prisma/migrations/20260415014750_tambah_table_app_and_enum_role/migration.sql b/prisma/migrations/20260415014750_tambah_table_app_and_enum_role/migration.sql new file mode 100644 index 0000000..6a14dba --- /dev/null +++ b/prisma/migrations/20260415014750_tambah_table_app_and_enum_role/migration.sql @@ -0,0 +1,40 @@ +/* + Warnings: + + - The values [USER,SUPER_ADMIN] on the enum `Role` will be removed. If these variants are still used in the database, this will fail. + - You are about to drop the column `app` on the `bug` table. All the data in the column will be lost. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "Role_new" AS ENUM ('ADMIN', 'DEVELOPER'); +ALTER TABLE "public"."user" ALTER COLUMN "role" DROP DEFAULT; +ALTER TABLE "user" ALTER COLUMN "role" TYPE "Role_new" USING ("role"::text::"Role_new"); +ALTER TYPE "Role" RENAME TO "Role_old"; +ALTER TYPE "Role_new" RENAME TO "Role"; +DROP TYPE "public"."Role_old"; +ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'ADMIN'; +COMMIT; + +-- AlterTable +ALTER TABLE "bug" DROP COLUMN "app", +ADD COLUMN "appId" TEXT; + +-- AlterTable +ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'ADMIN'; + +-- CreateTable +CREATE TABLE "App" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "version" TEXT NOT NULL, + "minVersion" TEXT NOT NULL, + "maintenance" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "App_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "bug" ADD CONSTRAINT "bug_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260415024317_update_enum_app/migration.sql b/prisma/migrations/20260415024317_update_enum_app/migration.sql new file mode 100644 index 0000000..9ce2368 --- /dev/null +++ b/prisma/migrations/20260415024317_update_enum_app/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "App" ALTER COLUMN "version" DROP NOT NULL, +ALTER COLUMN "minVersion" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 64b5f82..5667144 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,9 +9,7 @@ datasource db { } enum Role { - USER ADMIN - SUPER_ADMIN DEVELOPER } @@ -44,7 +42,7 @@ model User { name String email String @unique password String - role Role @default(USER) + role Role @default(ADMIN) active Boolean @default(true) image String? createdAt DateTime @default(now()) @@ -71,6 +69,19 @@ model Session { @@map("session") } +model App { + id String @id @default(uuid()) + name String + version String? + minVersion String? + maintenance Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + bugs Bug[] + +} + model Log { id String @id @default(uuid()) userId String @@ -86,12 +97,12 @@ model Log { model Bug { id String @id @default(uuid()) userId String? - app String? + appId String? affectedVersion String device String os String status BugStatus - source BugSource + source BugSource description String stackTrace String? fixedVersion String? @@ -100,6 +111,7 @@ model Bug { updatedAt DateTime @updatedAt user User? @relation(fields: [userId], references: [id]) + app App? @relation(fields: [appId], references: [id]) images BugImage[] logs BugLog[] diff --git a/prisma/seed.ts b/prisma/seed.ts index f90de89..31e64df 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -6,9 +6,7 @@ const SUPER_ADMIN_EMAILS = (process.env.SUPER_ADMIN_EMAIL ?? '').split(',').map( async function main() { const users = [ - { name: 'Super Admin', email: 'superadmin@example.com', password: 'superadmin123', role: 'SUPER_ADMIN' as const }, { name: 'Admin', email: 'admin@example.com', password: 'admin123', role: 'ADMIN' as const }, - { name: 'User', email: 'user@example.com', password: 'user123', role: 'USER' as const }, ] for (const u of users) { @@ -21,13 +19,28 @@ async function main() { console.log(`Seeded: ${u.email} (${u.role})`) } - // Promote super admin emails from env + // Promote DEVELOPER emails from env for (const email of SUPER_ADMIN_EMAILS) { - const user = await prisma.user.findUnique({ where: { email } }) - if (user && user.role !== 'SUPER_ADMIN') { - await prisma.user.update({ where: { email }, data: { role: 'SUPER_ADMIN' } }) - console.log(`Promoted to SUPER_ADMIN: ${email}`) - } + const password = await Bun.password.hash('developer123', { algorithm: 'bcrypt' }) + await prisma.user.upsert({ + where: { email }, + update: { role: 'DEVELOPER', password }, + create: { name: email.split('@')[0].toUpperCase(), email, password, role: 'DEVELOPER' }, + }) + console.log(`Promoted to DEVELOPER: ${email}`) + } + + const apps = [ + { id: 'desa-plus', name: 'Desa+' }, + ] + + for (const a of apps) { + await prisma.app.upsert({ + where: { id: a.id }, + update: { name: a.name }, + create: { id: a.id, name: a.name }, + }) + console.log(`Seeded: ${a.name}`) } } diff --git a/src/app.ts b/src/app.ts index b1a38ef..f860ae5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -37,8 +37,8 @@ export function createApp() { return { error: 'Email atau password salah' } } // Auto-promote super admin from env - if (env.SUPER_ADMIN_EMAILS.includes(user.email) && user.role !== 'SUPER_ADMIN') { - user = await prisma.user.update({ where: { id: user.id }, data: { role: 'SUPER_ADMIN' } }) + if (env.SUPER_ADMIN_EMAILS.includes(user.email) && user.role !== 'DEVELOPER') { + user = await prisma.user.update({ where: { id: user.id }, data: { role: 'DEVELOPER' } }) } const token = crypto.randomUUID() const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours @@ -78,80 +78,7 @@ export function createApp() { return { user: session.user } }) - // ─── Google OAuth ────────────────────────────────── - .get('/api/auth/google', ({ request, set }) => { - const origin = new URL(request.url).origin - const params = new URLSearchParams({ - client_id: env.GOOGLE_CLIENT_ID, - redirect_uri: `${origin}/api/auth/callback/google`, - response_type: 'code', - scope: 'openid email profile', - access_type: 'offline', - prompt: 'consent', - }) - set.status = 302; set.headers['location'] = `https://accounts.google.com/o/oauth2/v2/auth?${params}` - }) - .get('/api/auth/callback/google', async ({ request, set }) => { - const url = new URL(request.url) - const code = url.searchParams.get('code') - const origin = url.origin - - if (!code) { - set.status = 302; set.headers['location'] = '/login?error=google_failed' - return - } - - // Exchange code for tokens - const tokenRes = await fetch('https://oauth2.googleapis.com/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - code, - client_id: env.GOOGLE_CLIENT_ID, - client_secret: env.GOOGLE_CLIENT_SECRET, - redirect_uri: `${origin}/api/auth/callback/google`, - grant_type: 'authorization_code', - }), - }) - - if (!tokenRes.ok) { - set.status = 302; set.headers['location'] = '/login?error=google_failed' - return - } - - const tokens = (await tokenRes.json()) as { access_token: string } - - // Get user info - const userInfoRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { - headers: { Authorization: `Bearer ${tokens.access_token}` }, - }) - - if (!userInfoRes.ok) { - set.status = 302; set.headers['location'] = '/login?error=google_failed' - return - } - - const googleUser = (await userInfoRes.json()) as { email: string; name: string } - - // Upsert user (no password for Google users) - const isSuperAdmin = env.SUPER_ADMIN_EMAILS.includes(googleUser.email) - const user = await prisma.user.upsert({ - where: { email: googleUser.email }, - update: { name: googleUser.name, ...(isSuperAdmin ? { role: 'SUPER_ADMIN' } : {}) }, - create: { email: googleUser.email, name: googleUser.name, password: '', role: isSuperAdmin ? 'SUPER_ADMIN' : 'USER' }, - }) - - // Create session - const token = crypto.randomUUID() - const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) - await prisma.session.create({ data: { token, userId: user.id, expiresAt } }) - - await createSystemLog(user.id, 'LOGIN', 'Logged in via Google') - - set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400` - set.status = 302; set.headers['location'] = user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile' - }) // ─── Monitoring API ──────────────────────────────── .get('/api/dashboard/stats', async () => { @@ -172,7 +99,7 @@ export function createApp() { }) return bugs.map(b => ({ id: b.id, - app: b.app, + app: b.appId, message: b.description, version: b.affectedVersion, time: b.createdAt.toISOString(), @@ -180,18 +107,56 @@ export function createApp() { })) }) - .get('/api/apps', async () => { - const desaPlusErrors = await prisma.bug.count({ where: { app: { in: ['desa-plus', 'desa_plus'] }, status: 'OPEN' } }) - return [ - { id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: desaPlusErrors, version: '2.4.1' }, - ] + .get('/api/apps', async ({ query }) => { + const search = (query.search as string) || '' + const where: any = {} + if (search) { + where.name = { contains: search, mode: 'insensitive' } + } + + const apps = await prisma.app.findMany({ + where, + include: { + _count: { select: { bugs: true } }, + bugs: { where: { status: 'OPEN' }, select: { id: true } }, + }, + orderBy: { name: 'asc' }, + }) + + return apps.map((app) => ({ + id: app.id, + name: app.name, + status: app.maintenance ? 'warning' : app.bugs.length > 0 ? 'error' : 'active', + errors: app.bugs.length, + version: app.version ?? '-', + maintenance: app.maintenance, + })) }) - .get('/api/apps/:appId', ({ params: { appId } }) => { - const apps = { - 'desa-plus': { id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: 12, version: '2.4.1' }, + .get('/api/apps/:appId', async ({ params: { appId }, set }) => { + const app = await prisma.app.findUnique({ + where: { id: appId }, + include: { + _count: { select: { bugs: true } }, + bugs: { where: { status: 'OPEN' }, select: { id: true } }, + }, + }) + + if (!app) { + set.status = 404 + return { error: 'App not found' } + } + + return { + id: app.id, + name: app.name, + status: app.maintenance ? 'warning' : app.bugs.length > 0 ? 'error' : 'active', + errors: app.bugs.length, + version: app.version ?? '-', + minVersion: app.minVersion, + maintenance: app.maintenance, + totalBugs: app._count.bugs, } - return apps[appId as keyof typeof apps] || { id: appId, name: appId, status: 'active', users: 0, errors: 0, version: '1.0.0' } }) .get('/api/logs', async ({ query }) => { @@ -246,7 +211,7 @@ export function createApp() { } const body = (await request.json()) as { type: string, message: string } - const actingUserId = userId || (await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }))?.id || '' + const actingUserId = userId || (await prisma.user.findFirst({ where: { role: 'DEVELOPER' } }))?.id || '' await createSystemLog(actingUserId, body.type as any, body.message) return { ok: true } @@ -419,7 +384,7 @@ export function createApp() { ] } if (app && app !== 'all') { - where.app = app + where.appId = app } if (status && status !== 'all') { where.status = status @@ -463,12 +428,12 @@ export function createApp() { } const body = (await request.json()) as any - const defaultAdmin = await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }) + const defaultAdmin = await prisma.user.findFirst({ where: { role: 'DEVELOPER' } }) const actingUserId = userId || defaultAdmin?.id || '' const bug = await prisma.bug.create({ data: { - app: body.app, + appId: body.app, affectedVersion: body.affectedVersion, device: body.device, os: body.os, @@ -508,7 +473,7 @@ export function createApp() { } const body = (await request.json()) as { feedBack: string } - const defaultAdmin = await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }) + const defaultAdmin = await prisma.user.findFirst({ where: { role: 'DEVELOPER' } }) const actingUserId = userId || defaultAdmin?.id || undefined const bug = await prisma.bug.update({ @@ -538,7 +503,7 @@ export function createApp() { } const body = (await request.json()) as { status: string; description?: string } - const defaultAdmin = await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }) + const defaultAdmin = await prisma.user.findFirst({ where: { role: 'DEVELOPER' } }) const actingUserId = userId || defaultAdmin?.id || undefined const bug = await prisma.bug.update({ @@ -562,6 +527,30 @@ export function createApp() { return bug }) + // ─── System Status API ───────────────────────────── + .get('/api/system/status', async () => { + try { + // Check database connectivity + await prisma.$queryRaw`SELECT 1` + const activeSessions = await prisma.session.count({ + where: { expiresAt: { gte: new Date() } }, + }) + return { + status: 'operational', + database: 'connected', + activeSessions, + uptime: process.uptime(), + } + } catch { + return { + status: 'degraded', + database: 'disconnected', + activeSessions: 0, + uptime: process.uptime(), + } + } + }) + // ─── Example API ─────────────────────────────────── .get('/api/hello', () => ({ message: 'Hello, world!', diff --git a/src/frontend/components/AppCard.tsx b/src/frontend/components/AppCard.tsx index 3a8d485..7c5fcff 100644 --- a/src/frontend/components/AppCard.tsx +++ b/src/frontend/components/AppCard.tsx @@ -1,17 +1,18 @@ -import { Card, Group, Text, ThemeIcon, Badge, Avatar, Stack, Button, Progress, Box, useComputedColorScheme } from '@mantine/core' +import { Avatar, Button, Card, Group, Stack, Text, useComputedColorScheme } from '@mantine/core' import { Link } from '@tanstack/react-router' -import { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb' +import { TbChevronRight, TbDeviceMobile } from 'react-icons/tb' interface AppCardProps { id: string name: string status: 'active' | 'warning' | 'error' - users: number + users?: number errors: number version: string + maintenance?: boolean } -export function AppCard({ id, name, status, users, errors, version }: AppCardProps) { +export function AppCard({ id, name, status, errors, version }: AppCardProps) { const statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red' const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true }) @@ -46,12 +47,12 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro {name} - VERSION {version} + {/* VERSION {version} */} - + {/* {status.toUpperCase()} - + */} {/* diff --git a/src/frontend/components/DashboardLayout.tsx b/src/frontend/components/DashboardLayout.tsx index 5ec63f3..7829efb 100644 --- a/src/frontend/components/DashboardLayout.tsx +++ b/src/frontend/components/DashboardLayout.tsx @@ -1,4 +1,5 @@ import { APP_CONFIGS } from '@/frontend/config/appMenus' +import { useLogout, useSession } from '@/frontend/hooks/useAuth' import { ActionIcon, AppShell, @@ -7,6 +8,7 @@ import { Burger, Button, Group, + Loader, Menu, NavLink, Select, @@ -17,6 +19,7 @@ import { useMantineColorScheme } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' +import { useQuery } from '@tanstack/react-query' import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router' import { TbAlertTriangle, @@ -50,6 +53,26 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { const matches = useMatches() const currentPath = matches[matches.length - 1]?.pathname + // ─── Connect to auth system ────────────────────────── + const { data: sessionData } = useSession() + const user = sessionData?.user + const logout = useLogout() + + // ─── Fetch registered apps from database ───────────── + const { data: appsData } = useQuery({ + queryKey: ['apps'], + queryFn: () => fetch('/api/apps', { credentials: 'include' }).then((r) => r.json()), + staleTime: 60_000, + }) + + // ─── Fetch system status from database ─────────────── + const { data: systemStatus } = useQuery({ + queryKey: ['system', 'status'], + queryFn: () => fetch('/api/system/status', { credentials: 'include' }).then((r) => r.json()), + refetchInterval: 30_000, // refresh every 30 seconds + staleTime: 15_000, + }) + const globalNav = [ { label: 'Dashboard', icon: TbDashboard, to: '/dashboard' }, { label: 'Applications', icon: TbApps, to: '/apps' }, @@ -61,6 +84,21 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { const activeApp = appId ? APP_CONFIGS[appId] : null const navLinks = activeApp ? activeApp.menus : globalNav + // Build app selector data from API + const appSelectData = (appsData || []).map((app: any) => ({ + value: app.id, + label: app.name, + })) + + // System status indicator + const isOperational = systemStatus?.status === 'operational' + const statusColor = isOperational ? '#10b981' : '#f59e0b' + const statusText = isOperational ? 'All Systems Operational' : 'System Degraded' + + const handleLogout = () => { + logout.mutate() + } + return ( + > + {user?.name?.charAt(0).toUpperCase()} + + {user && ( + <> + + {user.name} + {user.email} + + + + )} Application - }>Profile - }>Settings + } + onClick={() => navigate({ to: '/profile' })} + > + Profile + + } + onClick={() => navigate({ to: '/dashboard' })} + > + Settings + Danger Zone - }> - Logout + } + onClick={handleLogout} + disabled={logout.isPending} + > + {logout.isPending ? 'Logging out...' : 'Logout'} @@ -160,10 +224,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { setCreateForm({ ...createForm, role: val || 'USER' })} @@ -461,10 +454,8 @@ function UsersPage() { ({ value: a.id, label: a.name })) || []} value={createForm.app} onChange={(val) => setCreateForm({ ...createForm, app: val as any })} + placeholder="Select application" + disabled={!appsList} /> ({ value: a.id, label: a.name })) || []} value={createForm.app} onChange={(val) => setCreateForm({ ...createForm, app: val as any })} + placeholder="Select application" + disabled={!appsList} />