Merge pull request 'amalia/15-apr-26' (#9) from amalia/15-apr-26 into main

Reviewed-on: #9
This commit is contained in:
2026-04-15 15:25:55 +08:00
39 changed files with 964 additions and 244 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
- generic [active]:
- generic:
- generic:
- generic: Loading...

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
- generic [active]:
- generic:
- generic:
- generic: Loading...

View File

@@ -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

13
.qwen/settings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--headless"
],
"timeout": 30000
}
},
"$version": 3
}

9
.qwen/settings.json.orig Normal file
View File

@@ -0,0 +1,9 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"],
"timeout": 30000
}
}
}

View File

@@ -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. - 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`. - 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 ## Testing
Tests use `bun:test`. Three levels: Tests use `bun:test`. Three levels:

100
PLAYWRIGHT_MCP.md Normal file
View File

@@ -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

BIN
bun.lockb

Binary file not shown.

35
login-snapshot.yml Normal file
View File

@@ -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

View File

@@ -18,7 +18,8 @@
"db:seed": "bun run prisma/seed.ts", "db:seed": "bun run prisma/seed.ts",
"db:studio": "bunx prisma studio", "db:studio": "bunx prisma studio",
"db:generate": "bunx prisma generate", "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": { "dependencies": {
"@elysiajs/cors": "^1.4.1", "@elysiajs/cors": "^1.4.1",
@@ -43,11 +44,14 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.10", "@biomejs/biome": "^2.4.10",
"@playwright/mcp": "^0.0.70",
"@playwright/test": "^1.59.1",
"@tanstack/router-vite-plugin": "^1.166.27", "@tanstack/router-vite-plugin": "^1.166.27",
"@types/bun": "latest", "@types/bun": "latest",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"playwright": "^1.59.1",
"prisma": "6", "prisma": "6",
"puppeteer-core": "^24.40.0", "puppeteer-core": "^24.40.0",
"typescript": "^6.0.2", "typescript": "^6.0.2",

File diff suppressed because one or more lines are too long

27
playwright.config.ts Normal file
View File

@@ -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,
},
});

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "App" ALTER COLUMN "version" DROP NOT NULL,
ALTER COLUMN "minVersion" DROP NOT NULL;

View File

@@ -9,9 +9,7 @@ datasource db {
} }
enum Role { enum Role {
USER
ADMIN ADMIN
SUPER_ADMIN
DEVELOPER DEVELOPER
} }
@@ -44,7 +42,7 @@ model User {
name String name String
email String @unique email String @unique
password String password String
role Role @default(USER) role Role @default(ADMIN)
active Boolean @default(true) active Boolean @default(true)
image String? image String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -71,6 +69,19 @@ model Session {
@@map("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 { model Log {
id String @id @default(uuid()) id String @id @default(uuid())
userId String userId String
@@ -86,7 +97,7 @@ model Log {
model Bug { model Bug {
id String @id @default(uuid()) id String @id @default(uuid())
userId String? userId String?
app String? appId String?
affectedVersion String affectedVersion String
device String device String
os String os String
@@ -100,6 +111,7 @@ model Bug {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
app App? @relation(fields: [appId], references: [id])
images BugImage[] images BugImage[]
logs BugLog[] logs BugLog[]

View File

@@ -6,9 +6,7 @@ const SUPER_ADMIN_EMAILS = (process.env.SUPER_ADMIN_EMAIL ?? '').split(',').map(
async function main() { async function main() {
const users = [ 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: '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) { for (const u of users) {
@@ -21,13 +19,28 @@ async function main() {
console.log(`Seeded: ${u.email} (${u.role})`) 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) { for (const email of SUPER_ADMIN_EMAILS) {
const user = await prisma.user.findUnique({ where: { email } }) const password = await Bun.password.hash('developer123', { algorithm: 'bcrypt' })
if (user && user.role !== 'SUPER_ADMIN') { await prisma.user.upsert({
await prisma.user.update({ where: { email }, data: { role: 'SUPER_ADMIN' } }) where: { email },
console.log(`Promoted to SUPER_ADMIN: ${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}`)
} }
} }

View File

@@ -37,8 +37,8 @@ export function createApp() {
return { error: 'Email atau password salah' } return { error: 'Email atau password salah' }
} }
// Auto-promote super admin from env // Auto-promote super admin from env
if (env.SUPER_ADMIN_EMAILS.includes(user.email) && user.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: 'SUPER_ADMIN' } }) user = await prisma.user.update({ where: { id: user.id }, data: { role: 'DEVELOPER' } })
} }
const token = crypto.randomUUID() const token = crypto.randomUUID()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
@@ -78,80 +78,7 @@ export function createApp() {
return { user: session.user } 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 ──────────────────────────────── // ─── Monitoring API ────────────────────────────────
.get('/api/dashboard/stats', async () => { .get('/api/dashboard/stats', async () => {
@@ -172,7 +99,7 @@ export function createApp() {
}) })
return bugs.map(b => ({ return bugs.map(b => ({
id: b.id, id: b.id,
app: b.app, app: b.appId,
message: b.description, message: b.description,
version: b.affectedVersion, version: b.affectedVersion,
time: b.createdAt.toISOString(), time: b.createdAt.toISOString(),
@@ -180,18 +107,56 @@ export function createApp() {
})) }))
}) })
.get('/api/apps', async () => { .get('/api/apps', async ({ query }) => {
const desaPlusErrors = await prisma.bug.count({ where: { app: { in: ['desa-plus', 'desa_plus'] }, status: 'OPEN' } }) const search = (query.search as string) || ''
return [ const where: any = {}
{ id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: desaPlusErrors, version: '2.4.1' }, 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' },
}) })
.get('/api/apps/:appId', ({ params: { appId } }) => { return apps.map((app) => ({
const apps = { id: app.id,
'desa-plus': { id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: 12, version: '2.4.1' }, 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', 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 }) => { .get('/api/logs', async ({ query }) => {
@@ -246,7 +211,7 @@ export function createApp() {
} }
const body = (await request.json()) as { type: string, message: string } 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) await createSystemLog(actingUserId, body.type as any, body.message)
return { ok: true } return { ok: true }
@@ -419,7 +384,7 @@ export function createApp() {
] ]
} }
if (app && app !== 'all') { if (app && app !== 'all') {
where.app = app where.appId = app
} }
if (status && status !== 'all') { if (status && status !== 'all') {
where.status = status where.status = status
@@ -463,12 +428,12 @@ export function createApp() {
} }
const body = (await request.json()) as any 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 actingUserId = userId || defaultAdmin?.id || ''
const bug = await prisma.bug.create({ const bug = await prisma.bug.create({
data: { data: {
app: body.app, appId: body.app,
affectedVersion: body.affectedVersion, affectedVersion: body.affectedVersion,
device: body.device, device: body.device,
os: body.os, os: body.os,
@@ -508,7 +473,7 @@ export function createApp() {
} }
const body = (await request.json()) as { feedBack: string } 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 actingUserId = userId || defaultAdmin?.id || undefined
const bug = await prisma.bug.update({ const bug = await prisma.bug.update({
@@ -538,7 +503,7 @@ export function createApp() {
} }
const body = (await request.json()) as { status: string; description?: string } 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 actingUserId = userId || defaultAdmin?.id || undefined
const bug = await prisma.bug.update({ const bug = await prisma.bug.update({
@@ -562,6 +527,30 @@ export function createApp() {
return bug 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 ─────────────────────────────────── // ─── Example API ───────────────────────────────────
.get('/api/hello', () => ({ .get('/api/hello', () => ({
message: 'Hello, world!', message: 'Hello, world!',

View File

@@ -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 { Link } from '@tanstack/react-router'
import { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb' import { TbChevronRight, TbDeviceMobile } from 'react-icons/tb'
interface AppCardProps { interface AppCardProps {
id: string id: string
name: string name: string
status: 'active' | 'warning' | 'error' status: 'active' | 'warning' | 'error'
users: number users?: number
errors: number errors: number
version: string 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 statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red'
const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true }) const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
@@ -46,12 +47,12 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
</Avatar> </Avatar>
<Stack gap={0}> <Stack gap={0}>
<Text fw={700} size="lg" style={{ letterSpacing: '-0.3px' }}>{name}</Text> <Text fw={700} size="lg" style={{ letterSpacing: '-0.3px' }}>{name}</Text>
<Text size="xs" c="dimmed" fw={600}>VERSION {version}</Text> {/* <Text size="xs" c="dimmed" fw={600}>VERSION {version}</Text> */}
</Stack> </Stack>
</Group> </Group>
<Badge color={statusColor} variant="dot" size="sm"> {/* <Badge color={statusColor} variant="dot" size="sm">
{status.toUpperCase()} {status.toUpperCase()}
</Badge> </Badge> */}
</Group> </Group>
{/* <Stack gap="md" mt="sm"> {/* <Stack gap="md" mt="sm">

View File

@@ -1,4 +1,5 @@
import { APP_CONFIGS } from '@/frontend/config/appMenus' import { APP_CONFIGS } from '@/frontend/config/appMenus'
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
import { import {
ActionIcon, ActionIcon,
AppShell, AppShell,
@@ -7,6 +8,7 @@ import {
Burger, Burger,
Button, Button,
Group, Group,
Loader,
Menu, Menu,
NavLink, NavLink,
Select, Select,
@@ -17,6 +19,7 @@ import {
useMantineColorScheme useMantineColorScheme
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { useQuery } from '@tanstack/react-query'
import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router' import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router'
import { import {
TbAlertTriangle, TbAlertTriangle,
@@ -50,6 +53,26 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const matches = useMatches() const matches = useMatches()
const currentPath = matches[matches.length - 1]?.pathname 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 = [ const globalNav = [
{ label: 'Dashboard', icon: TbDashboard, to: '/dashboard' }, { label: 'Dashboard', icon: TbDashboard, to: '/dashboard' },
{ label: 'Applications', icon: TbApps, to: '/apps' }, { label: 'Applications', icon: TbApps, to: '/apps' },
@@ -61,6 +84,21 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const activeApp = appId ? APP_CONFIGS[appId] : null const activeApp = appId ? APP_CONFIGS[appId] : null
const navLinks = activeApp ? activeApp.menus : globalNav 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 ( return (
<AppShell <AppShell
header={{ height: 70 }} header={{ height: 70 }}
@@ -115,21 +153,47 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
<Menu.Target> <Menu.Target>
<Avatar <Avatar
src={undefined} src={undefined}
alt="User" alt={user?.name || 'User'}
color="brand-blue" color="brand-blue"
radius="xl" radius="xl"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
/> >
{user?.name?.charAt(0).toUpperCase()}
</Avatar>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
{user && (
<>
<Menu.Label>
<Text size="sm" fw={600} truncate>{user.name}</Text>
<Text size="xs" c="dimmed" truncate>{user.email}</Text>
</Menu.Label>
<Menu.Divider />
</>
)}
<Menu.Label>Application</Menu.Label> <Menu.Label>Application</Menu.Label>
<Menu.Item leftSection={<TbUserCircle size={16} />}>Profile</Menu.Item> <Menu.Item
<Menu.Item leftSection={<TbSettings size={16} />}>Settings</Menu.Item> leftSection={<TbUserCircle size={16} />}
onClick={() => navigate({ to: '/profile' })}
>
Profile
</Menu.Item>
<Menu.Item
leftSection={<TbSettings size={16} />}
onClick={() => navigate({ to: '/dashboard' })}
>
Settings
</Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Label>Danger Zone</Menu.Label> <Menu.Label>Danger Zone</Menu.Label>
<Menu.Item color="red" leftSection={<TbLogout size={16} />}> <Menu.Item
Logout color="red"
leftSection={<TbLogout size={16} />}
onClick={handleLogout}
disabled={logout.isPending}
>
{logout.isPending ? 'Logging out...' : 'Logout'}
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
@@ -160,10 +224,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
<Select <Select
label="Selected Application" label="Selected Application"
value={appId} value={appId}
data={[ data={appSelectData.length > 0 ? appSelectData : [
{ value: 'desa-plus', label: 'Desa+' }, { value: 'desa-plus', label: 'Desa+' },
{ value: 'e-commerce', label: 'E-Commerce' },
{ value: 'fitness-app', label: 'Fitness App' },
]} ]}
onChange={(val) => val && navigate({ to: '/apps/$appId', params: { appId: val } })} onChange={(val) => val && navigate({ to: '/apps/$appId', params: { appId: val } })}
radius="md" radius="md"
@@ -225,19 +287,26 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
> >
<Text size="xs" c="dimmed" fw={600} mb="xs">SYSTEM STATUS</Text> <Text size="xs" c="dimmed" fw={600} mb="xs">SYSTEM STATUS</Text>
<Group gap="xs"> <Group gap="xs">
<Box style={{ width: 8, height: 8, borderRadius: '50%', background: '#10b981' }} /> <Box style={{ width: 8, height: 8, borderRadius: '50%', background: statusColor, boxShadow: `0 0 6px ${statusColor}` }} />
<Text size="sm" fw={500}>All Systems Operational</Text> <Text size="sm" fw={500}>{statusText}</Text>
</Group> </Group>
{systemStatus && (
<Text size="xs" c="dimmed" mt={4}>
{systemStatus.activeSessions} active session{systemStatus.activeSessions !== 1 ? 's' : ''}
</Text>
)}
</Box> </Box>
<Button <Button
variant="light" variant="light"
color="red" color="red"
fullWidth fullWidth
leftSection={<TbLogout size={16} />} leftSection={logout.isPending ? <Loader size={16} color="red" /> : <TbLogout size={16} />}
mt="md" mt="md"
onClick={handleLogout}
disabled={logout.isPending}
> >
Log out {logout.isPending ? 'Logging out...' : 'Log out'}
</Button> </Button>
</Stack> </Stack>
</Box> </Box>

View File

@@ -15,62 +15,35 @@ import {
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { useState } from 'react' import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { TbMessageReport, TbHistory, TbExternalLink, TbBug } from 'react-icons/tb' import { TbMessageReport, TbHistory, TbExternalLink, TbBug } from 'react-icons/tb'
const mockErrors = [ export interface ErrorDataTableProps {
{ appId?: string
id: 1, }
message: 'NullPointerException at village_sync.dart:45',
village: 'Sukatani',
version: 'v1.2.0',
timestamp: '2026-04-01 14:30:15',
severity: 'critical',
stackTrace: 'at com.desa.sync.VillageManager.sync(VillageManager.java:45)\nat com.desa.sync.SyncService.onHandleIntent(SyncService.java:120)'
},
{
id: 2,
message: 'Failed to load citizen record session',
village: 'Sukamaju',
version: 'v1.1.8',
timestamp: '2026-04-01 14:15:22',
severity: 'high',
stackTrace: 'Error: Connection timeout reaching upstream citizen-db\n at HttpClient.get (network.dart:88)'
},
{
id: 3,
message: 'SocketException: Connection timed out',
village: 'Cikini',
version: 'v1.2.0',
timestamp: '2026-04-01 13:55:10',
severity: 'medium',
stackTrace: 'SocketException: OS Error: Connection timed out, errno = 110, address = 10.0.2.2, port = 54332'
},
{
id: 4,
message: 'UI Thread blocking > 500ms',
village: 'Beji',
version: 'v1.1.2',
timestamp: '2026-04-01 13:40:00',
severity: 'low',
stackTrace: 'ANR (Application Not Responding) detected in main thread.'
},
]
export function ErrorDataTable() { export function ErrorDataTable({ appId }: ErrorDataTableProps) {
const [opened, { open, close }] = useDisclosure(false) const [opened, { open, close }] = useDisclosure(false)
const [selectedError, setSelectedError] = useState<any>(null) const [selectedError, setSelectedError] = useState<any>(null)
const { data: bugsData, isLoading } = useQuery({
queryKey: ['bugs', appId],
queryFn: () => fetch(`/api/bugs?app=${appId || 'all'}&limit=10`).then((r) => r.json()),
})
const bugs = bugsData?.data || []
const handleRowClick = (error: any) => { const handleRowClick = (error: any) => {
setSelectedError(error) setSelectedError(error)
open() open()
} }
const getSeverityColor = (sev: string) => { const getSeverityColor = (sev: string) => {
switch(sev) { switch(sev?.toUpperCase()) {
case 'critical': return 'red' case 'OPEN': return 'red'
case 'high': return 'orange' case 'IN_PROGRESS': return 'orange'
case 'medium': return 'yellow' case 'ON_HOLD': return 'yellow'
default: return 'gray' default: return 'gray'
} }
} }
@@ -86,7 +59,7 @@ export function ErrorDataTable() {
</ThemeIcon> </ThemeIcon>
<Text fw={700}>LATEST ERROR REPORTS</Text> <Text fw={700}>LATEST ERROR REPORTS</Text>
</Group> </Group>
<Button component={Link} to='/apps/desa-plus/errors' variant="subtle" size="compact-xs" color="blue" rightSection={<TbExternalLink size={14} />}> <Button component={Link} to={appId ? `/apps/${appId}/errors` : '/bug-reports'} variant="subtle" size="compact-xs" color="blue" rightSection={<TbExternalLink size={14} />}>
View All Reports View All Reports
</Button> </Button>
</Group> </Group>
@@ -97,37 +70,49 @@ export function ErrorDataTable() {
<Table.Thead bg="rgba(0,0,0,0.1)"> <Table.Thead bg="rgba(0,0,0,0.1)">
<Table.Tr> <Table.Tr>
<Table.Th px="xl">Error Message</Table.Th> <Table.Th px="xl">Error Message</Table.Th>
<Table.Th>Village</Table.Th> <Table.Th>Reporter</Table.Th>
<Table.Th>App Version</Table.Th> <Table.Th>App Version</Table.Th>
<Table.Th>Timestamp</Table.Th> <Table.Th>Timestamp</Table.Th>
<Table.Th pr="xl">Severity</Table.Th> <Table.Th pr="xl">Severity</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{mockErrors.map((error) => ( {isLoading ? (
<Table.Tr>
<Table.Td colSpan={5} align="center" py="xl">
Loading errors...
</Table.Td>
</Table.Tr>
) : bugs.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={5} align="center" py="xl">
No errors found.
</Table.Td>
</Table.Tr>
) : bugs.map((error: any) => (
<Table.Tr <Table.Tr
key={error.id} key={error.id}
onClick={() => handleRowClick(error)} onClick={() => handleRowClick(error)}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >
<Table.Td px="xl"> <Table.Td px="xl">
<Text size="sm" fw={600} lineClamp={1}>{error.message}</Text> <Text size="sm" fw={600} lineClamp={1}>{error.description}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Badge variant="dot" color="brand-blue" radius="sm">{error.village}</Badge> <Badge variant="dot" color="brand-blue" radius="sm">{error.user?.name || error.userId || 'System'}</Badge>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text size="xs" fw={700} c="dimmed">{error.version}</Text> <Text size="xs" fw={700} c="dimmed">{error.affectedVersion || 'N/A'}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Group gap={6}> <Group gap={6}>
<TbHistory size={12} color="gray" /> <TbHistory size={12} color="gray" />
<Text size="xs" c="dimmed">{error.timestamp}</Text> <Text size="xs" c="dimmed">{new Date(error.createdAt).toLocaleString()}</Text>
</Group> </Group>
</Table.Td> </Table.Td>
<Table.Td pr="xl"> <Table.Td pr="xl">
<Badge color={getSeverityColor(error.severity)} variant="light" size="sm"> <Badge color={getSeverityColor(error.status)} variant="light" size="sm">
{error.severity.toUpperCase()} {(error.status || '').toUpperCase()}
</Badge> </Badge>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
@@ -156,17 +141,17 @@ export function ErrorDataTable() {
<Stack p="lg" gap="xl"> <Stack p="lg" gap="xl">
<Box> <Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>MESSAGE</Text> <Text size="xs" fw={700} c="dimmed" mb={4}>MESSAGE</Text>
<Text fw={700} size="lg" color="red">{selectedError.message}</Text> <Text fw={700} size="lg" color="red">{selectedError.description}</Text>
</Box> </Box>
<SimpleGrid cols={2} spacing="lg"> <SimpleGrid cols={2} spacing="lg">
<Box> <Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>VILLAGE</Text> <Text size="xs" fw={700} c="dimmed" mb={4}>REPORTER</Text>
<Text fw={600}>{selectedError.village}</Text> <Text fw={600}>{selectedError.user?.name || selectedError.userId || 'System'}</Text>
</Box> </Box>
<Box> <Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>APP VERSION</Text> <Text size="xs" fw={700} c="dimmed" mb={4}>APP VERSION</Text>
<Badge variant="outline">{selectedError.version}</Badge> <Badge variant="outline">{selectedError.affectedVersion || 'N/A'}</Badge>
</Box> </Box>
</SimpleGrid> </SimpleGrid>

View File

@@ -1,7 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
export type Role = 'USER' | 'ADMIN' | 'SUPER_ADMIN' export type Role = | 'ADMIN' | 'DEVELOPER'
export interface User { export interface User {
id: string id: string
@@ -41,12 +41,7 @@ export function useLogin() {
}), }),
onSuccess: (data) => { onSuccess: (data) => {
queryClient.setQueryData(['auth', 'session'], data) queryClient.setQueryData(['auth', 'session'], data)
// Super admin → dashboard, others → profile
if (data.user.role === 'SUPER_ADMIN') {
navigate({ to: '/dashboard' }) navigate({ to: '/dashboard' })
} else {
navigate({ to: '/profile' })
}
}, },
}) })
} }

View File

@@ -68,6 +68,12 @@ function AppErrorsPage() {
fetch(API_URLS.getBugs(page, search, app, status)).then((r) => r.json()), fetch(API_URLS.getBugs(page, search, app, status)).then((r) => r.json()),
}) })
// Fetch apps for the dropdown
const { data: appsList } = useQuery({
queryKey: ['apps-list'],
queryFn: () => fetch('/api/apps').then((r) => r.json()),
})
// Create Bug Modal Logic // Create Bug Modal Logic
const [opened, { open, close }] = useDisclosure(false) const [opened, { open, close }] = useDisclosure(false)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@@ -344,12 +350,11 @@ function AppErrorsPage() {
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<Select <Select
label="Application" label="Application"
data={[ data={appsList?.map((a: any) => ({ value: a.id, label: a.name })) || []}
{ value: 'desa-plus', label: 'Desa+' },
{ value: 'hipmi', label: 'Hipmi' },
]}
value={createForm.app} value={createForm.app}
onChange={(val) => setCreateForm({ ...createForm, app: val as any })} onChange={(val) => setCreateForm({ ...createForm, app: val as any })}
placeholder="Select application"
disabled={!appsList}
/> />
<Select <Select
label="Source" label="Source"

View File

@@ -1,3 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts' import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
import { ErrorDataTable } from '@/frontend/components/ErrorDataTable' import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
import { SummaryCard } from '@/frontend/components/SummaryCard' import { SummaryCard } from '@/frontend/components/SummaryCard'
@@ -51,6 +52,11 @@ function AppOverviewPage() {
const { data: dailyRes, isLoading: dailyLoading, mutate: mutateDaily } = useSWR(isDesaPlus ? API_URLS.getDailyActivity() : null, fetcher) const { data: dailyRes, isLoading: dailyLoading, mutate: mutateDaily } = useSWR(isDesaPlus ? API_URLS.getDailyActivity() : null, fetcher)
const { data: comparisonRes, isLoading: comparisonLoading, mutate: mutateComparison } = useSWR(isDesaPlus ? API_URLS.getComparisonActivity() : null, fetcher) const { data: comparisonRes, isLoading: comparisonLoading, mutate: mutateComparison } = useSWR(isDesaPlus ? API_URLS.getComparisonActivity() : null, fetcher)
const { data: appData, isLoading: appLoading } = useQuery({
queryKey: ['apps', appId],
queryFn: () => fetch(`/api/apps/${appId}`).then((r) => r.json()),
})
const grid = gridRes?.data const grid = gridRes?.data
const dailyData = dailyRes?.data || [] const dailyData = dailyRes?.data || []
const comparisonData = comparisonRes?.data || [] const comparisonData = comparisonRes?.data || []
@@ -209,12 +215,11 @@ function AppOverviewPage() {
</SummaryCard> </SummaryCard>
<SummaryCard <SummaryCard
title="Errors Today" title="Errors Open"
value="12" value={appLoading ? '...' : (appData?.errors || '0')}
icon={TbAlertTriangle} icon={TbAlertTriangle}
color="red" color="red"
isError={true} isError={true}
trend={{ value: '4.8%', positive: false }}
/> />
</SimpleGrid> </SimpleGrid>
@@ -223,7 +228,7 @@ function AppOverviewPage() {
<VillageComparisonBarChart data={comparisonData} isLoading={comparisonLoading} /> <VillageComparisonBarChart data={comparisonData} isLoading={comparisonLoading} />
</SimpleGrid> </SimpleGrid>
<ErrorDataTable /> <ErrorDataTable appId={appId} />
</Stack> </Stack>
</> </>
) )

View File

@@ -1,5 +1,7 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Container, Stack, Title, Text, SimpleGrid, Group, Button, TextInput, Loader } from '@mantine/core' import { Container, Stack, Title, Text, SimpleGrid, Group, Button, TextInput, Loader } from '@mantine/core'
import { useDebouncedValue } from '@mantine/hooks'
import { useState } from 'react'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { TbPlus, TbSearch } from 'react-icons/tb' import { TbPlus, TbSearch } from 'react-icons/tb'
import { DashboardLayout } from '@/frontend/components/DashboardLayout' import { DashboardLayout } from '@/frontend/components/DashboardLayout'
@@ -10,9 +12,12 @@ export const Route = createFileRoute('/apps/')({
}) })
function AppsPage() { function AppsPage() {
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 300)
const { data: apps, isLoading } = useQuery({ const { data: apps, isLoading } = useQuery({
queryKey: ['apps'], queryKey: ['apps', debouncedSearch],
queryFn: () => fetch('/api/apps').then((r) => r.json()), queryFn: () => fetch(`/api/apps?search=${encodeURIComponent(debouncedSearch)}`).then((r) => r.json()),
}) })
return ( return (
@@ -24,14 +29,14 @@ function AppsPage() {
<Title order={2} className="gradient-text">Applications</Title> <Title order={2} className="gradient-text">Applications</Title>
<Text size="sm" c="dimmed">Manage and monitor all your mobile applications from one place.</Text> <Text size="sm" c="dimmed">Manage and monitor all your mobile applications from one place.</Text>
</Stack> </Stack>
<Button {/* <Button
variant="gradient" variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }} gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />} leftSection={<TbPlus size={18} />}
radius="md" radius="md"
> >
Add New Application Add New Application
</Button> </Button> */}
</Group> </Group>
<Group> <Group>
@@ -40,6 +45,8 @@ function AppsPage() {
leftSection={<TbSearch size={16} />} leftSection={<TbSearch size={16} />}
style={{ flex: 1 }} style={{ flex: 1 }}
radius="md" radius="md"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/> />
</Group> </Group>

View File

@@ -66,6 +66,12 @@ function ListErrorsPage() {
fetch(API_URLS.getBugs(page, search, app, status)).then((r) => r.json()), fetch(API_URLS.getBugs(page, search, app, status)).then((r) => r.json()),
}) })
// Fetch apps for the dropdown
const { data: appsList } = useQuery({
queryKey: ['apps-list'],
queryFn: () => fetch('/api/apps').then((r) => r.json()),
})
// Create Bug Modal Logic // Create Bug Modal Logic
const [opened, { open, close }] = useDisclosure(false) const [opened, { open, close }] = useDisclosure(false)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@@ -353,12 +359,11 @@ function ListErrorsPage() {
<SimpleGrid cols={2}> <SimpleGrid cols={2}>
<Select <Select
label="Application" label="Application"
data={[ data={appsList?.map((a: any) => ({ value: a.id, label: a.name })) || []}
{ value: 'desa-plus', label: 'Desa+' },
{ value: 'hipmi', label: 'Hipmi' },
]}
value={createForm.app} value={createForm.app}
onChange={(val) => setCreateForm({ ...createForm, app: val as any })} onChange={(val) => setCreateForm({ ...createForm, app: val as any })}
placeholder="Select application"
disabled={!appsList}
/> />
<Select <Select
label="Source" label="Source"
@@ -451,12 +456,12 @@ function ListErrorsPage() {
placeholder="Application" placeholder="Application"
data={[ data={[
{ value: 'all', label: 'All Applications' }, { value: 'all', label: 'All Applications' },
{ value: 'desa-plus', label: 'Desa+' }, ...(appsList?.map((a: any) => ({ value: a.id, label: a.name })) || []),
{ value: 'hipmi', label: 'Hipmi' },
]} ]}
value={app} value={app}
onChange={(val) => setApp(val || 'all')} onChange={(val) => setApp(val || 'all')}
radius="md" radius="md"
disabled={!appsList}
/> />
<Select <Select
placeholder="Status" placeholder="Status"

View File

@@ -27,7 +27,6 @@ export const Route = createFileRoute('/dashboard')({
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()), queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
}) })
if (!data?.user) throw redirect({ to: '/login' }) if (!data?.user) throw redirect({ to: '/login' })
if (data.user.role !== 'SUPER_ADMIN') throw redirect({ to: '/profile' })
} catch (e) { } catch (e) {
if (e instanceof Error) throw redirect({ to: '/login' }) if (e instanceof Error) throw redirect({ to: '/login' })
throw e throw e

View File

@@ -1,3 +1,4 @@
import { useLogin } from '@/frontend/hooks/useAuth'
import { import {
Alert, Alert,
Button, Button,
@@ -13,8 +14,7 @@ import {
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { useState } from 'react' import { useState } from 'react'
import { FcGoogle } from 'react-icons/fc' import { FcGoogle } from 'react-icons/fc'
import { TbAlertCircle, TbLogin, TbLock, TbMail } from 'react-icons/tb' import { TbAlertCircle, TbLock, TbLogin, TbMail } from 'react-icons/tb'
import { useLogin } from '@/frontend/hooks/useAuth'
export const Route = createFileRoute('/login')({ export const Route = createFileRoute('/login')({
validateSearch: (search: Record<string, unknown>): { error?: string } => ({ validateSearch: (search: Record<string, unknown>): { error?: string } => ({
@@ -27,7 +27,7 @@ export const Route = createFileRoute('/login')({
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()), queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
}) })
if (data?.user) { if (data?.user) {
throw redirect({ to: data.user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile' }) throw redirect({ to: '/dashboard' })
} }
} catch (e) { } catch (e) {
if (e instanceof Error) return if (e instanceof Error) return
@@ -57,12 +57,6 @@ function LoginPage() {
Login Login
</Title> </Title>
<Text c="dimmed" size="sm" ta="center">
Demo: <strong>superadmin@example.com</strong> / <strong>superadmin123</strong>
<br />
or: <strong>user@example.com</strong> / <strong>user123</strong>
</Text>
{(login.isError || searchError) && ( {(login.isError || searchError) && (
<Alert icon={<TbAlertCircle size={16} />} color="red" variant="light"> <Alert icon={<TbAlertCircle size={16} />} color="red" variant="light">
{login.isError ? login.error.message : 'Google login failed, please try again.'} {login.isError ? login.error.message : 'Google login failed, please try again.'}
@@ -95,18 +89,6 @@ function LoginPage() {
> >
Sign in Sign in
</Button> </Button>
<Divider label="or" labelPosition="center" />
<Button
component="a"
href="/api/auth/google"
fullWidth
variant="default"
leftSection={<FcGoogle size={18} />}
>
Login with Google
</Button>
</Stack> </Stack>
</form> </form>
</Paper> </Paper>

View File

@@ -30,9 +30,8 @@ export const Route = createFileRoute('/profile')({
}) })
const roleBadgeColor: Record<string, string> = { const roleBadgeColor: Record<string, string> = {
USER: 'blue',
ADMIN: 'violet', ADMIN: 'violet',
SUPER_ADMIN: 'red', DEVELOPER: 'red',
} }
function ProfilePage() { function ProfilePage() {

View File

@@ -59,20 +59,15 @@ const getRoleColor = (role: string) => {
} }
const roles = [ const roles = [
{
name: 'SUPER_ADMIN',
color: 'red',
permissions: ['Full Access', 'User Mgmt', 'Role Mgmt', 'App Config', 'Logs & Errors']
},
{ {
name: 'DEVELOPER', name: 'DEVELOPER',
color: 'brand-blue', color: 'red',
permissions: ['View All Apps', 'Manage Assigned App', 'View Logs', 'Resolve Errors', 'Village Setup'] permissions: ['Full Access', 'Error Feedback', 'Error Management', 'App Version Management', 'User Management']
}, },
{ {
name: 'QA', name: 'ADMIN',
color: 'orange', color: 'orange',
permissions: ['View All Apps', 'View Logs', 'Report Errors', 'Test App Features'] permissions: ['View All Apps', 'View Logs', 'Report Errors']
}, },
] ]
@@ -414,10 +409,8 @@ function UsersPage() {
<Select <Select
label="Role" label="Role"
data={[ data={[
{ value: 'USER', label: 'User' },
{ value: 'ADMIN', label: 'Admin' }, { value: 'ADMIN', label: 'Admin' },
{ value: 'DEVELOPER', label: 'Developer' }, { value: 'DEVELOPER', label: 'Developer' },
{ value: 'SUPER_ADMIN', label: 'Super Admin' },
]} ]}
value={createForm.role} value={createForm.role}
onChange={(val) => setCreateForm({ ...createForm, role: val || 'USER' })} onChange={(val) => setCreateForm({ ...createForm, role: val || 'USER' })}
@@ -461,10 +454,8 @@ function UsersPage() {
<Select <Select
label="Role" label="Role"
data={[ data={[
{ value: 'USER', label: 'User' },
{ value: 'ADMIN', label: 'Admin' }, { value: 'ADMIN', label: 'Admin' },
{ value: 'DEVELOPER', label: 'Developer' }, { value: 'DEVELOPER', label: 'Developer' },
{ value: 'SUPER_ADMIN', label: 'Super Admin' },
]} ]}
value={editForm.role} value={editForm.role}
onChange={(val) => setEditForm({ ...editForm, role: val || 'USER' })} onChange={(val) => setEditForm({ ...editForm, role: val || 'USER' })}

View File

@@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}

View File

@@ -9,7 +9,7 @@ export function createTestApp() {
} }
/** Create a test user with hashed password, returns the user record */ /** Create a test user with hashed password, returns the user record */
export async function seedTestUser(email = 'test@example.com', password = 'test123', name = 'Test User', role: 'USER' | 'ADMIN' | 'SUPER_ADMIN' = 'USER') { export async function seedTestUser(email = 'test@example.com', password = 'test123', name = 'Test User', role: 'ADMIN' | 'DEVELOPER' = 'DEVELOPER') {
const hashed = await Bun.password.hash(password, { algorithm: 'bcrypt' }) const hashed = await Bun.password.hash(password, { algorithm: 'bcrypt' })
return prisma.user.upsert({ return prisma.user.upsert({
where: { email }, where: { email },