From 0152229b966468ce528a52be419fef7e6513742e Mon Sep 17 00:00:00 2001 From: bipproduction Date: Fri, 6 Feb 2026 07:02:41 +0800 Subject: [PATCH] feat: complete lid format handling and UI updates --- .../whatsapp-web/.claude-plugin/plugin.json | 35 ++ .agents/skills/whatsapp-web/SKILL.md | 142 +++++ .qwen/skills/whatsapp-web | 1 + bun.lock | 30 +- package.json | 4 +- src/App.tsx | 1 + src/index.html | 2 +- src/pages/Home.tsx | 164 +++++- src/pages/Login.tsx | 128 +++- src/pages/sq/dashboard/apikey/apikey_page.tsx | 486 +++++++++------- src/pages/sq/dashboard/dashboard_layout.tsx | 348 +++++------ src/pages/sq/dashboard/dashboard_page.tsx | 218 ++++++- .../sq/dashboard/wa-hook/wa_hook_home.tsx | 375 ++++++------ .../sq/dashboard/wa-hook/wa_hook_layout.tsx | 4 +- src/pages/sq/dashboard/wajs/wajs_home.tsx | 325 ++++++++++- src/pages/sq/dashboard/wajs/wajs_layout.tsx | 208 +++++-- .../sq/dashboard/webhook/webhook_create.tsx | 463 ++++++--------- .../sq/dashboard/webhook/webhook_edit.tsx | 549 ++++++++---------- .../sq/dashboard/webhook/webhook_home.tsx | 383 ++++++------ .../sq/dashboard/webhook/webhook_layout.tsx | 15 +- src/pages/wajs/qrcode.tsx | 138 ++++- src/server/lib/wa/wa_service.ts | 366 ++++++------ x.ts | 39 ++ x.tsx | 218 ------- 24 files changed, 2719 insertions(+), 1923 deletions(-) create mode 100644 .agents/skills/whatsapp-web/.claude-plugin/plugin.json create mode 100644 .agents/skills/whatsapp-web/SKILL.md create mode 120000 .qwen/skills/whatsapp-web create mode 100644 x.ts delete mode 100644 x.tsx diff --git a/.agents/skills/whatsapp-web/.claude-plugin/plugin.json b/.agents/skills/whatsapp-web/.claude-plugin/plugin.json new file mode 100644 index 0000000..42acb3e --- /dev/null +++ b/.agents/skills/whatsapp-web/.claude-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "whatsapp-web", + "version": "1.0.0", + "description": "Automate WhatsApp messaging, groups, and media sharing via web interface", + "author": "Canifi", + "category": "communication", + "permissions": [ + "browser", + "notifications", + "env-access" + ], + "triggers": [ + "whatsapp", + "whatsapp message", + "send whatsapp", + "whatsapp group", + "whatsapp status" + ], + "authentication": { + "type": "qr-code", + "loginUrl": "https://web.whatsapp.com", + "sessionIndicator": "#app .app-wrapper-web" + }, + "requiredEnvVars": [ + "WHATSAPP_LINKED" + ], + "capabilities": { + "messaging": true, + "groups": true, + "media": true, + "status": true, + "search": true, + "broadcast": true + } +} diff --git a/.agents/skills/whatsapp-web/SKILL.md b/.agents/skills/whatsapp-web/SKILL.md new file mode 100644 index 0000000..3408a2e --- /dev/null +++ b/.agents/skills/whatsapp-web/SKILL.md @@ -0,0 +1,142 @@ +--- +name: whatsapp-web +description: Enables Claude to send messages, manage groups, and handle WhatsApp communications through the web interface +version: 1.0.0 +author: Canifi +category: communication +--- + +# WhatsApp Web Skill + +## Overview +Automates WhatsApp Web interactions including messaging, group management, status updates, and media sharing through browser automation with QR code authentication. + +## Quick Install + +```bash +curl -sSL https://canifi.com/skills/whatsapp-web/install.sh | bash +``` + +Or manually: +```bash +cp -r skills/whatsapp-web ~/.canifi/skills/ +``` + +## Setup + +Configure via [canifi-env](https://canifi.com/setup/scripts): + +```bash +# First, ensure canifi-env is installed: +# curl -sSL https://canifi.com/install.sh | bash + +canifi-env set WHATSAPP_LINKED "true" +``` + +## Privacy & Authentication + +**Your credentials, your choice.** Canifi LifeOS respects your privacy. + +### Option 1: Manual Browser Login (Recommended) +If you prefer not to share credentials with Claude Code: +1. Complete the [Browser Automation Setup](/setup/automation) using CDP mode +2. Login to the service manually in the Playwright-controlled Chrome window +3. Claude will use your authenticated session without ever seeing your password + +### Option 2: Environment Variables +If you're comfortable sharing credentials, you can store them locally: +```bash +canifi-env set SERVICE_EMAIL "your-email" +canifi-env set SERVICE_PASSWORD "your-password" +``` + +**Note**: Credentials stored in canifi-env are only accessible locally on your machine and are never transmitted. + +## Capabilities +- Send and receive text messages +- Share images, videos, and documents +- Create and manage groups +- Update and view status/stories +- Search messages and contacts +- Star and archive chats +- Manage group settings and admins +- Handle broadcast lists + +## Usage Examples + +### Example 1: Send a Message +``` +User: "Send a WhatsApp message to Sarah asking about dinner plans" +Claude: I'll send that message on WhatsApp. +- Navigate to web.whatsapp.com +- Search for Sarah in contacts +- Open chat and compose message +- Send: "Hey Sarah, what are the dinner plans for tonight?" +- Confirm message delivered (double checkmarks) +``` + +### Example 2: Create a Group +``` +User: "Create a WhatsApp group called 'Weekend Trip' with John, Mike, and Lisa" +Claude: I'll create that group. +- Click New Group +- Add John, Mike, and Lisa +- Set group name "Weekend Trip" +- Add group icon if available +- Confirm group created +``` + +### Example 3: Share a File +``` +User: "Send the project proposal PDF to the Work group on WhatsApp" +Claude: I'll share that document. +- Open Work group chat +- Click attach file +- Select and upload proposal PDF +- Add caption if needed +- Send file to group +``` + +### Example 4: Check Unread Messages +``` +User: "Check my unread WhatsApp messages and summarize them" +Claude: I'll check your unread messages. +- Identify chats with unread indicators +- Open each unread chat +- Read and collect new messages +- Present summary of conversations +``` + +## Authentication Flow +1. Navigate to web.whatsapp.com via Playwright MCP +2. If QR code shown, notify user via iMessage to scan with phone +3. Wait for phone authentication (timeout: 60 seconds) +4. Verify chat list loads successfully +5. Maintain session via local storage +6. Re-authenticate if session expires + +## Error Handling +- **QR Code Timeout**: iMessage reminder to scan QR code +- **Session Expired**: Notify user to re-link device +- **Phone Disconnected**: Alert user that phone must be online +- **Rate Limited**: Wait and implement backoff +- **Contact Not Found**: Search by phone number or name variations +- **Group Limit Reached**: Notify user of WhatsApp limits (1024 members) +- **Media Failed**: Check file size and format, retry upload +- **Connection Lost**: Wait for reconnection, notify if persistent + +## Self-Improvement Instructions +When encountering new WhatsApp features: +1. Document new UI elements and chat patterns +2. Add support for new message types (polls, etc.) +3. Log successful group management patterns +4. Update for new WhatsApp Web features + +## Notes +- WhatsApp Web requires phone to be connected to internet +- End-to-end encryption maintained through web interface +- Status/stories expire after 24 hours +- Broadcast lists have recipient limits +- Some features require WhatsApp Business +- Voice and video calls not supported via web automation +- Multi-device beta allows operation without phone online diff --git a/.qwen/skills/whatsapp-web b/.qwen/skills/whatsapp-web new file mode 120000 index 0000000..227c0a2 --- /dev/null +++ b/.qwen/skills/whatsapp-web @@ -0,0 +1 @@ +../../.agents/skills/whatsapp-web \ No newline at end of file diff --git a/bun.lock b/bun.lock index 3a44dd2..fbfcb31 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,8 @@ "@elysiajs/swagger": "^1.3.1", "@lglab/react-qr-code": "^1.4.9", "@mantine/core": "^8.3.13", + "@mantine/dates": "^8.3.14", + "@mantine/form": "^8.3.14", "@mantine/hooks": "^8.3.13", "@mantine/modals": "^8.3.13", "@mantine/notifications": "^8.3.13", @@ -49,7 +51,7 @@ "uuid": "^13.0.0", "whatsapp-api-js": "^6.2.1", "whatsapp-client-sdk": "^1.6.0", - "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js#main", + "whatsapp-web.js": "^1.34.6", "yaml": "^2.8.2", }, "devDependencies": { @@ -65,7 +67,7 @@ }, }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -151,6 +153,10 @@ "@mantine/core": ["@mantine/core@8.3.13", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.13", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-ZgW4vqN4meaPyIMxzAufBvsgmJRfYZdTpsrAOcS8pWy7m9e8i685E7XsAxnwJCOIHudpvpvt+7Bx7VaIjEsYEw=="], + "@mantine/dates": ["@mantine/dates@8.3.14", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "@mantine/core": "8.3.14", "@mantine/hooks": "8.3.14", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-NdStRo2ZQ55MoMF5B9vjhpBpHRDHF1XA9Dkb1kKSdNuLlaFXKlvoaZxj/3LfNPpn7Nqlns78nWt4X8/cgC2YIg=="], + + "@mantine/form": ["@mantine/form@8.3.14", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-LJUeab+oF+YzATrm/K03Z/QoVVYlaolWqLUZZj7XexNA4hS2/ycKyWT07YhGkdHTLXkf3DUtrg1sS77K7Oje8A=="], + "@mantine/hooks": ["@mantine/hooks@8.3.13", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-7YMbMW/tR9E8m/9DbBW01+3RNApm2mE/JbRKXf9s9+KxgbjQvq0FYGWV8Y4+Sjz48AO4vtWk2qBriUTgBMKAyg=="], "@mantine/modals": ["@mantine/modals@8.3.13", "", { "peerDependencies": { "@mantine/core": "8.3.13", "@mantine/hooks": "8.3.13", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-5jIRJKEupQerHfPGcPHgQk+J6dGBO7spC66VgEZqCNRpbWhowCxBNGEW5LN1hZE9sLYBJg+z2MazPws4A1GohQ=="], @@ -181,7 +187,7 @@ "@prisma/get-platform": ["@prisma/get-platform@6.19.2", "", { "dependencies": { "@prisma/debug": "6.19.2" } }, "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA=="], - "@puppeteer/browsers": ["@puppeteer/browsers@2.11.1", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.3", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-YmhAxs7XPuxN0j7LJloHpfD1ylhDuFmmwMvfy/+6nBSrETT2ycL53LrhgPtR+f+GcPSybQVuQ5inWWu5MrWCpA=="], + "@puppeteer/browsers": ["@puppeteer/browsers@2.12.0", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.3", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw=="], "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], @@ -279,7 +285,7 @@ "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], - "bare-fs": ["bare-fs@4.5.2", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw=="], + "bare-fs": ["bare-fs@4.5.3", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ=="], "bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="], @@ -325,7 +331,7 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chromium-bidi": ["chromium-bidi@12.0.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg=="], + "chromium-bidi": ["chromium-bidi@13.1.0", "", { "dependencies": { "mitt": "^3.0.1", "puppeteer": "^24.36.0", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-IdGNojX6S04+wgJOALzvkkIyLelhEGqI8xSctwiYJJGSi9T2eBjwAQW2UjBD/mCXv/rUkNlH2+h7jz+58vT74A=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], @@ -391,7 +397,7 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "devtools-protocol": ["devtools-protocol@0.0.1534754", "", {}, "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ=="], + "devtools-protocol": ["devtools-protocol@0.0.1566079", "", {}, "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ=="], "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], @@ -449,6 +455,8 @@ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], @@ -549,6 +557,8 @@ "jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="], + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -685,9 +695,9 @@ "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], - "puppeteer": ["puppeteer@24.35.0", "", { "dependencies": { "@puppeteer/browsers": "2.11.1", "chromium-bidi": "12.0.1", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1534754", "puppeteer-core": "24.35.0", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-sbjB5JnJ+3nwgSdRM/bqkFXqLxRz/vsz0GRIeTlCk+j+fGpqaF2dId9Qp25rXz9zfhqnN9s0krek1M/C2GDKtA=="], + "puppeteer": ["puppeteer@24.37.0", "", { "dependencies": { "@puppeteer/browsers": "2.12.0", "chromium-bidi": "13.1.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1566079", "puppeteer-core": "24.37.0", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-s1jHugVhPtQjiJE6wUyonj4VEGWF+mfRDASqPMPsXgKcjZX0GaznBmcT9nLQ7bBL90phuQUqO4jiV5vTecZg4g=="], - "puppeteer-core": ["puppeteer-core@24.35.0", "", { "dependencies": { "@puppeteer/browsers": "2.11.1", "chromium-bidi": "12.0.1", "debug": "^4.4.3", "devtools-protocol": "0.0.1534754", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.3.10", "ws": "^8.19.0" } }, "sha512-vt1zc2ME0kHBn7ZDOqLvgvrYD5bqNv5y2ZNXzYnCv8DEtZGw/zKhljlrGuImxptZ4rq+QI9dFGrUIYqG4/IQzA=="], + "puppeteer-core": ["puppeteer-core@24.37.0", "", { "dependencies": { "@puppeteer/browsers": "2.12.0", "chromium-bidi": "13.1.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1566079", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.4.0", "ws": "^8.19.0" } }, "sha512-WoCBK36cBlbaxwuvPWhOp2+lR6O6ynHdDuvD8rEIkxPOPpUoMXSJuyiOWhHtexJBCLaMCAJk33QdYambvQl+og=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], @@ -841,7 +851,7 @@ "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.3.10", "", {}, "sha512-5LAE43jAVLOhB/QqX4bwSiv0Hg1HBfMmOuwBSXHdvg4GMGu9Y0lIq7p4R/yySu6w74WmaR4GM4H9t2IwLW7hgw=="], + "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.0", "", {}, "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -849,7 +859,7 @@ "whatsapp-client-sdk": ["whatsapp-client-sdk@1.6.0", "", { "dependencies": { "axios": "^1.6.0", "form-data": "^4.0.0", "uuid": "^9.0.0" }, "peerDependencies": { "@supabase/supabase-js": "^2.0.0" }, "optionalPeers": ["@supabase/supabase-js"] }, "sha512-iAbdpv8tmw3RLPUxmw2ha8qbkKCuijbgl/HiDt4xpHgw3IWL3bB5o1SYX3qwuuEWwalXSNDiOu6W7lARJ8+XJw=="], - "whatsapp-web.js": ["whatsapp-web.js@github:pedroslopez/whatsapp-web.js#dd9df40", { "dependencies": { "@pedroslopez/moduleraid": "^5.0.2", "fluent-ffmpeg": "2.1.3", "mime": "^3.0.0", "node-fetch": "^2.6.9", "node-webpmux": "3.1.7", "puppeteer": "^24.31.0" }, "optionalDependencies": { "archiver": "^5.3.1", "fs-extra": "^10.1.0", "unzipper": "^0.10.11" } }, "pedroslopez-whatsapp-web.js-dd9df40"], + "whatsapp-web.js": ["whatsapp-web.js@1.34.6", "", { "dependencies": { "@pedroslopez/moduleraid": "^5.0.2", "fluent-ffmpeg": "2.1.3", "mime": "^3.0.0", "node-fetch": "^2.6.9", "node-webpmux": "3.1.7", "puppeteer": "^24.31.0" }, "optionalDependencies": { "archiver": "^5.3.1", "fs-extra": "^10.1.0", "unzipper": "^0.10.11" } }, "sha512-+zgLBqARcVfuCG7b80c7Gkt+4Yh8w+oDWx7lL2gTA6nlaykHBne7NwJ5yGe2r7O9IYraIzs6HiCzNGKfu9AUBg=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], diff --git a/package.json b/package.json index 0aa9b44..efee708 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@elysiajs/swagger": "^1.3.1", "@lglab/react-qr-code": "^1.4.9", "@mantine/core": "^8.3.13", + "@mantine/dates": "^8.3.14", + "@mantine/form": "^8.3.14", "@mantine/hooks": "^8.3.13", "@mantine/modals": "^8.3.13", "@mantine/notifications": "^8.3.13", @@ -55,7 +57,7 @@ "uuid": "^13.0.0", "whatsapp-api-js": "^6.2.1", "whatsapp-client-sdk": "^1.6.0", - "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js#main", + "whatsapp-web.js": "^1.34.6", "yaml": "^2.8.2" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index cb39c60..a7ca127 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import "@mantine/core/styles.css"; import "@mantine/notifications/styles.css"; +import '@mantine/dates/styles.css'; import { Notifications } from "@mantine/notifications"; import { ModalsProvider } from "@mantine/modals"; import { MantineProvider } from "@mantine/core"; diff --git a/src/index.html b/src/index.html index 2a957d0..8e872fb 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ - + diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 3077f69..5934b4a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,15 +1,163 @@ import clientRoutes from "@/clientRoutes"; -import { Button, Container } from "@mantine/core"; +import { + Button, + Container, + Text, + Title, + Stack, + Group, + SimpleGrid, + ThemeIcon, + Paper, + Box, + rem, + Divider, + Badge, +} from "@mantine/core"; +import { + IconBrandWhatsapp, + IconRocket, + IconShieldCheck, + IconPlugConnected, + IconArrowRight, +} from "@tabler/icons-react"; import { useNavigate } from "react-router-dom"; +const features = [ + { + icon: IconBrandWhatsapp, + title: "WhatsApp Integration", + description: "Connect and automate WhatsApp messages effortlessly using wa-web.js technology.", + color: "green", + }, + { + icon: IconPlugConnected, + title: "Webhooks & API", + description: "Integrate with your existing systems via powerful webhooks and a developer-friendly API.", + color: "blue", + }, + { + icon: IconRocket, + title: "High Performance", + description: "Built on Bun and ElysiaJS for maximum speed and efficient resource management.", + color: "orange", + }, + { + icon: IconShieldCheck, + title: "Secure & Reliable", + description: "Manage API keys and sessions securely with a robust management dashboard.", + color: "teal", + }, +]; + export default function Home() { const navigate = useNavigate(); + return ( - -

Home

- -
+ + {/* Hero Section */} + + + + + + + + + Master Your{" "} + <Text + component="span" + variant="gradient" + gradient={{ from: "green", to: "teal" }} + inherit + > + WhatsApp + </Text>{" "} + Workflow + + + + + A robust, full-stack WhatsApp integration platform built with Bun. + Send messages, manage webhooks, and automate your communication + with ease. + + + + + + + + + + + + {/* Features Section */} + + + + {features.map((feature) => ( + + + + + + {feature.title} + + + {feature.description} + + + ))} + + + + + {/* Footer */} + + + + + ยฉ 2026 wajs-server. Built with Bun & Mantine. + + + Production Ready + v1.0.0 + + + + ); -} +} \ No newline at end of file diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 8f95e7e..b07c9dd 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,28 +1,43 @@ import { Button, Container, - Group, - PasswordInput, - Stack, + Paper, Text, TextInput, + PasswordInput, + Group, + Stack, + Title, + Center, + Box, + ThemeIcon, } from "@mantine/core"; import { useEffect, useState } from "react"; +import { useForm } from "@mantine/form"; +import { notifications } from "@mantine/notifications"; +import { IconAt, IconLock, IconLogin, IconBrandWhatsapp } from "@tabler/icons-react"; import apiFetch from "../lib/apiFetch"; import clientRoutes from "@/clientRoutes"; import { Navigate } from "react-router-dom"; export default function Login() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); - const [isAuthenticated, setIsAuthenticated] = useState(null); + const form = useForm({ + initialValues: { + email: "", + password: "", + }, + validate: { + email: (value) => (/^\S+@\S+$/.test(value) ? null : "Invalid email"), + password: (value) => (value.length < 1 ? "Password is required" : null), + }, + }); + useEffect(() => { async function checkSession() { try { - // backend otomatis baca cookie JWT dari request const res = await apiFetch.api.user.find.get(); setIsAuthenticated(res.status === 200); } catch { @@ -32,54 +47,103 @@ export default function Login() { checkSession(); }, []); - const handleSubmit = async () => { + const handleSubmit = async (values: typeof form.values) => { setLoading(true); try { const response = await apiFetch.auth.login.post({ - email, - password, + email: values.email, + password: values.password, }); if (response.data?.token) { localStorage.setItem("token", response.data.token); + notifications.show({ + title: "Login Successful", + message: "Welcome back!", + color: "green", + }); window.location.href = clientRoutes["/sq/dashboard"]; return; } if (response.error) { - alert(JSON.stringify(response.error)); + notifications.show({ + title: "Login Failed", + message: (response.error as any)?.value?.message || "Invalid credentials", + color: "red", + }); } } catch (error) { console.error(error); + notifications.show({ + title: "Error", + message: "An unexpected error occurred", + color: "red", + }); } finally { setLoading(false); } }; - if (isAuthenticated === null) return null; // or loading spinner + if (isAuthenticated === null) return null; if (isAuthenticated) return ; return ( - - - Login - setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> - - - - - + + + + + + + + Welcome Back! + + + Login to manage your WhatsApp integration + + + + +
+ + } + {...form.getInputProps("email")} + /> + } + {...form.getInputProps("password")} + /> + + + + +
+
+
+
); } diff --git a/src/pages/sq/dashboard/apikey/apikey_page.tsx b/src/pages/sq/dashboard/apikey/apikey_page.tsx index 38fa504..ad1975d 100644 --- a/src/pages/sq/dashboard/apikey/apikey_page.tsx +++ b/src/pages/sq/dashboard/apikey/apikey_page.tsx @@ -1,306 +1,342 @@ +import apiFetch from "@/lib/apiFetch"; import { + ActionIcon, + Badge, + Box, Button, - Card, + Center, Container, + Divider, Group, + Loader, + Paper, + ScrollArea, Stack, Table, Text, TextInput, - ScrollArea, - Divider, - Tooltip, - Badge, - Loader, - ActionIcon, - Center, + Title, + Tooltip } from "@mantine/core"; -import { IconKey, IconPlus, IconTrash, IconCopy } from "@tabler/icons-react"; +import { DateInput } from "@mantine/dates"; +import { useForm } from "@mantine/form"; +import { notifications } from "@mantine/notifications"; +import { + IconCalendar, + IconCopy, + IconInfoCircle, + IconKey, + IconPlus, + IconSearch, + IconTrash, +} from "@tabler/icons-react"; +import dayjs from "dayjs"; import { useEffect, useState } from "react"; -import { showNotification } from "@mantine/notifications"; -import apiFetch from "@/lib/apiFetch"; export default function ApiKeyPage() { + const [refresh, setRefresh] = useState(false); + + const toggleRefresh = () => setRefresh((r) => !r); + return ( - + - - - - - API Key Management - + + + + + + + API Key Management + + + + Generate and manage secure access keys for your integrations. + + + } + > + Secure Access + - - Secure Access - - - - + + + + + ); } -function CreateApiKey() { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [expiredAt, setExpiredAt] = useState(""); +function CreateApiKey({ onCreated }: { onCreated: () => void }) { const [loading, setLoading] = useState(false); - const [refresh, setRefresh] = useState(false); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim()) { - showNotification({ - title: "Missing name", - message: "Please enter a name for your API key", + const form = useForm({ + initialValues: { + name: "", + description: "", + expiredAt: null as Date | null, + }, + validate: { + name: (value) => (value.trim().length < 3 ? "Name must be at least 3 characters" : null), + }, + }); + + const handleSubmit = async (values: typeof form.values) => { + setLoading(true); + try { + const res = await apiFetch.api.apikey.create.post({ + name: values.name, + description: values.description, + expiredAt: values.expiredAt ? values.expiredAt.toISOString() : undefined, + }); + + if (res.status === 200) { + form.reset(); + notifications.show({ + title: "Success", + message: "API key created successfully", + color: "teal", + }); + onCreated(); + } else { + notifications.show({ + title: "Error", + message: (res.error as any)?.message || "Failed to create API key", + color: "red", + }); + } + } catch (err) { + console.error(err); + notifications.show({ + title: "Error", + message: "An unexpected error occurred", color: "red", }); - return; - } - setLoading(true); - const res = await apiFetch.api.apikey.create.post({ - name, - description, - expiredAt, - }); - setLoading(false); - if (res.status === 200) { - setName(""); - setDescription(""); - setExpiredAt(""); - showNotification({ - title: "Success", - message: "API key created successfully", - color: "teal", - }); - setRefresh((r) => !r); + } finally { + setLoading(false); } }; return ( - - - - - - Create New API Key - - - -
- - setName(e.target.value)} - required - /> - setDescription(e.target.value)} - /> - setExpiredAt(e.target.value)} - /> - - - - - -
-
-
+ + + + + Create New API Key + + + - - +
+ + + + } + clearable + {...form.getInputProps("expiredAt")} + /> + + + + + + +
+
+ ); } function ListApiKey({ refresh }: { refresh: boolean }) { const [apiKeys, setApiKeys] = useState([]); const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); - useEffect(() => { - const fetchApiKeys = async () => { - setLoading(true); + const fetchApiKeys = async () => { + setLoading(true); + try { const res = await apiFetch.api.apikey.list.get(); if (res.status === 200) { setApiKeys(res.data?.apiKeys || []); } + } catch (err) { + console.error(err); + } finally { setLoading(false); - }; + } + }; + + useEffect(() => { fetchApiKeys(); }, [refresh]); + const filteredKeys = apiKeys.filter((key) => + key.name.toLowerCase().includes(search.toLowerCase()) + ); + + const handleDelete = async (id: string) => { + try { + const res = await apiFetch.api.apikey.delete.delete({ id }); + if (res.status === 200) { + setApiKeys((prev) => prev.filter((a) => a.id !== id)); + notifications.show({ + title: "Deleted", + message: "API key removed successfully", + color: "red", + }); + } + } catch (err) { + console.error(err); + notifications.show({ + title: "Error", + message: "Failed to delete API key", + color: "red", + }); + } + }; + + const handleCopy = (key: string) => { + navigator.clipboard.writeText(key); + notifications.show({ + title: "Copied", + message: "API key copied to clipboard", + color: "teal", + icon: , + }); + }; + return ( - + - + Active API Keys + } + value={search} + onChange={(e) => setSearch(e.target.value)} + size="xs" + w={250} + /> - + + + {loading ? ( -
- +
+ + + + Fetching your keys... + +
- ) : apiKeys.length === 0 ? ( -
- No API keys found + ) : filteredKeys.length === 0 ? ( +
+ + + + {search ? "No keys match your search" : "No API keys created yet"} + +
) : ( - +
Name Description - Expired + Expiration Created - Updated - Actions + + Actions + - {apiKeys.map((apiKey: any, index: number) => ( - - {apiKey.name} - {apiKey.description || "โ€”"} + {filteredKeys.map((apiKey: any) => ( + - {apiKey.expiredAt - ? new Date(apiKey.expiredAt).toISOString().split("T")[0] - : "โ€”"} + + {apiKey.name} + - {new Date(apiKey.createdAt).toISOString().split("T")[0]} + + {apiKey.description || "No description"} + - {new Date(apiKey.updatedAt).toISOString().split("T")[0]} + {apiKey.expiredAt ? ( + + {dayjs(apiKey.expiredAt).format("MMM DD, YYYY")} + + ) : ( + + Never + + )} - - - + + {dayjs(apiKey.createdAt).format("MMM DD, YYYY")} + + + + { - navigator.clipboard.writeText(apiKey.key); - showNotification({ - title: "Copied", - message: "API key copied to clipboard", - color: "teal", - }); - }} + onClick={() => handleCopy(apiKey.key)} + size="lg" > - + { - await apiFetch.api.apikey.delete.delete({ - id: apiKey.id, - }); - setApiKeys((prev) => - prev.filter((a) => a.id !== apiKey.id), - ); - showNotification({ - title: "Deleted", - message: "API key removed successfully", - color: "red", - }); - }} + onClick={() => handleDelete(apiKey.id)} + size="lg" > @@ -314,6 +350,6 @@ function ListApiKey({ refresh }: { refresh: boolean }) { )} - + ); } diff --git a/src/pages/sq/dashboard/dashboard_layout.tsx b/src/pages/sq/dashboard/dashboard_layout.tsx index fecdc34..6dbc9b8 100644 --- a/src/pages/sq/dashboard/dashboard_layout.tsx +++ b/src/pages/sq/dashboard/dashboard_layout.tsx @@ -1,177 +1,113 @@ -import { useEffect, useState } from "react"; +import clientRoutes from "@/clientRoutes"; +import apiFetch from "@/lib/apiFetch"; import { - ActionIcon, AppShell, Avatar, - Button, - Card, + Box, + Burger, Divider, - Flex, Group, + Menu, NavLink, - Paper, ScrollArea, Stack, Text, + ThemeIcon, Title, - Tooltip, - Badge, + UnstyledButton, + rem } from "@mantine/core"; -import { useLocalStorage } from "@mantine/hooks"; +import { useDisclosure } from "@mantine/hooks"; import { - IconChevronLeft, + IconBrandWhatsapp, + IconChevronDown, IconChevronRight, IconDashboard, + IconHome, IconKey, - IconWebhook, - IconBrandWhatsapp, - IconUser, IconLogout, + IconSettings, + IconWebhook, } from "@tabler/icons-react"; import type { User } from "generated/prisma"; +import { useEffect, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import apiFetch from "@/lib/apiFetch"; -import clientRoutes from "@/clientRoutes"; - -function Logout() { - return ( - - - - ); -} export default function DashboardLayout() { - const [opened, setOpened] = useLocalStorage({ - key: "nav_open", - defaultValue: true, - }); + const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); + const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); + const location = useLocation(); return ( - - - - - setOpened((v) => !v)} - aria-label="Toggle navigation" - style={{ - color: "#00FFC8", - background: "rgba(0,255,200,0.1)", - // boxShadow: "0 0 10px rgba(0,255,200,0.2)", - }} - > - {opened ? : } - - + + + + + + + + + + + WAJS SERVER + + - - + + + + + + + + - + + - - - - - {!opened && ( - - setOpened(true)} - aria-label="Open navigation" - style={{ - color: "#00FFFF", - background: "rgba(0,255,200,0.1)", - }} - > - - - - )} - - Control Center - - - Live - - - + + - + ); } -function HostView() { +function HostHeaderView() { const [host, setHost] = useState(null); + const navigate = useNavigate(); useEffect(() => { async function fetchHost() { @@ -181,51 +117,49 @@ function HostView() { fetchHost(); }, []); + const handleLogout = async () => { + await apiFetch.auth.logout.delete(); + localStorage.removeItem("token"); + window.location.href = "/login"; + }; + + if (!host) return null; + return ( - - {host ? ( - - - + + + + + {host.name?.[0]} - - + + {host.name} - - {host.email} - - - - - - - ) : ( - - Host data unavailable - - )} - + + + + + + + + Application + } + > + Settings + + + } + > + Logout + + + ); } @@ -237,61 +171,63 @@ function NavigationDashboard() { { path: "/sq/dashboard/dashboard", label: "Overview", - icon: , - desc: "Main dashboard insights", + icon: , + }, + { + path: "/sq/dashboard/wajs/wajs-home", + label: "WhatsApp Service", + icon: , + }, + { + path: "/sq/dashboard/wa-hook/wa-hook-home", + label: "Hook Activity", + icon: , + }, + { + path: "/sq/dashboard/webhook/webhook-home", + label: "Webhooks Config", + icon: , }, { path: "/sq/dashboard/apikey/apikey", label: "API Keys", - icon: , - desc: "Manage and regenerate access tokens", - }, - { - path: "/sq/dashboard/wajs/wajs-home", - label: "Wajs Integration", - icon: , - desc: "WhatsApp session manager", - }, - { - path: "/sq/dashboard/webhook/webhook-home", - label: "Webhooks", - icon: , - desc: "Incoming and outgoing event handlers", - }, - { - path: clientRoutes["/sq/dashboard/wa-hook/wa-hook-home"], - label: "WA Hook", - icon: , - desc: "WA Hook", + icon: , }, ]; return ( - + {items.map((item) => ( navigate(clientRoutes[item.path as keyof typeof clientRoutes]) } - style={{ - borderRadius: "12px", - color: "#EAEAEA", - background: location.pathname.startsWith(item.path) - ? "rgba(0,255,200,0.15)" - : "transparent", - transition: "background 0.2s ease", - }} - styles={{ - label: { fontWeight: 500, color: "#EAEAEA" }, - description: { color: "#9A9A9A" }, - }} + variant="light" + color="teal" + style={{ borderRadius: rem(8) }} + rightSection={} /> ))} ); } + +function NavigationFooter() { + const navigate = useNavigate(); + return ( + + } + label="Back to Home" + onClick={() => navigate("/")} + variant="subtle" + color="gray" + style={{ borderRadius: rem(8) }} + /> + + ); +} diff --git a/src/pages/sq/dashboard/dashboard_page.tsx b/src/pages/sq/dashboard/dashboard_page.tsx index 97210bc..d6249e6 100644 --- a/src/pages/sq/dashboard/dashboard_page.tsx +++ b/src/pages/sq/dashboard/dashboard_page.tsx @@ -1,7 +1,219 @@ +import { + Title, + Text, + Container, + Grid, + Paper, + Group, + Stack, + ThemeIcon, + Badge, + Button, + Box, + SimpleGrid, + List, + ThemeIcon as MantineThemeIcon, + rem, +} from "@mantine/core"; +import { + IconDashboard, + IconBrandWhatsapp, + IconKey, + IconWebhook, + IconCircleCheck, + IconCircleX, + IconPlayerPlay, + IconArrowRight, + IconSettings, +} from "@tabler/icons-react"; +import useSWR from "swr"; +import apiFetch from "@/lib/apiFetch"; +import clientRoutes from "@/clientRoutes"; +import { useNavigate } from "react-router-dom"; + export default function Dashboard() { + const navigate = useNavigate(); + const { data: waState } = useSWR("/wa/state", apiFetch.api.wa.state.get, { + refreshInterval: 5000, + }); + + const isWaReady = waState?.data?.state?.ready; + return ( -
-

Dashboard

-
+ + {/* Header Section */} + + + + + System Overview + + + Welcome to your WhatsApp Integration Control Center. + + + + System {isWaReady ? "Online" : "Action Required"} + + + + + {/* Main Stats / Status Cards */} + + navigate(clientRoutes["/sq/dashboard/wajs/wajs-home"])} + /> + navigate(clientRoutes["/sq/dashboard/apikey/apikey"])} + /> + navigate(clientRoutes["/sq/dashboard/webhook/webhook-home"])} + /> + + + + {/* Getting Started / Guide */} + + + Getting Started + + + + + } + > + + Scan WhatsApp QR Code + Go to WhatsApp Service and scan the QR to link your device. + + + Generate API Key + Create a secure key to authenticate your external requests. + + + Configure Webhooks + Set up URLs to receive real-time notifications for incoming messages. + + + Start Automation + Your system is now ready to send and receive messages automatically. + + + + + + + + + {/* Quick Tips / Info */} + + + + + + + + Developer Pro Tip + + + You can use the API Key in the "Authorization" header as a Bearer token + to send messages programmatically via our REST API. + + + + + System Health + + + + + + + + + + + ); +} + +interface StatusCardProps { + title: string; + status: string; + color: string; + icon: React.ElementType; + description: string; + actionLabel: string; + onAction: () => void; +} + +function StatusCard({ title, status, color, icon: Icon, description, actionLabel, onAction }: StatusCardProps) { + return ( + + + + + + + {status} + + + + {title} + {description} + + + + + + ); +} + +function HealthItem({ label, status, color }: { label: string; status: string; color: string }) { + return ( + + {label} + + + {status} + + ); } diff --git a/src/pages/sq/dashboard/wa-hook/wa_hook_home.tsx b/src/pages/sq/dashboard/wa-hook/wa_hook_home.tsx index 0c48957..72a9500 100644 --- a/src/pages/sq/dashboard/wa-hook/wa_hook_home.tsx +++ b/src/pages/sq/dashboard/wa-hook/wa_hook_home.tsx @@ -1,32 +1,42 @@ import apiFetch from "@/lib/apiFetch"; import { + ActionIcon, + Avatar, + Badge, + Box, Button, - Card, - Container, + Center, + Divider, Group, Pagination, + Paper, + rem, Skeleton, Stack, Text, + ThemeIcon, Title, - Badge, - ScrollArea, - Tooltip, - Divider, + Tooltip } from "@mantine/core"; import { useLocalStorage, useShallowEffect } from "@mantine/hooks"; -import { showNotification } from "@mantine/notifications"; +import { modals } from "@mantine/modals"; +import { notifications } from "@mantine/notifications"; import { - IconRefresh, - IconMessageCircle, - IconUser, - IconCalendar, + IconActivity, IconHash, - IconCode, + IconMessageCircle, + IconPhone, + IconRefresh, + IconRobot, + IconTrash, + IconUser } from "@tabler/icons-react"; import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; import useSWR from "swr"; +dayjs.extend(relativeTime); + export default function WaHookHome() { const [page, setPage] = useLocalStorage({ key: "wa-hook-page", defaultValue: 1 }); const { data, error, isLoading, mutate } = useSWR( @@ -43,195 +53,202 @@ export default function WaHookHome() { mutate(); }, [page]); - async function handleReset() { - await apiFetch["wa-hook"].reset.post(); - mutate(); - showNotification({ - title: "Reset Completed", - message: "All WhatsApp Hook data has been cleared.", - color: "teal", + const handleReset = () => { + modals.openConfirmModal({ + title: "Clear Activity Logs", + centered: true, + children: ( + + Are you sure you want to clear all WhatsApp activity logs? This action cannot be undone. + + ), + labels: { confirm: "Clear All", cancel: "Cancel" }, + confirmProps: { color: "red" }, + onConfirm: async () => { + await apiFetch["wa-hook"].reset.post(); + mutate(); + notifications.show({ + title: "Logs Cleared", + message: "All activity data has been successfully removed.", + color: "teal", + }); + }, }); - } + }; + + if (isLoading) + return ( + + + + + ); - if (isLoading) return ; if (error) return ( - - - Failed to load webhook data. - - +
+ + Failed to load activity data + + +
); return ( - - - - - - WhatsApp Hook Monitor - - - Real-time webhook activity and message tracking + + + + + + + + + + Hook Activity Monitor + + + + Track real-time WhatsApp messages and AI flow responses. - + + + mutate()}> + + + - + + + - + + {data?.data?.list?.length ? ( + data.data.list.map((item) => { + let parsed: any = {}; + try { + parsed = typeof item.data === 'string' ? JSON.parse(item.data) : item.data; + } catch (e) { + parsed = {}; + } - {/*
{JSON.stringify(data?.data?.list, null, 2)}
*/} + return ( + + + + + + + + + + {parsed.name || "Unknown Sender"} + + + + + {parsed.number || "No number"} + + + + + + + {dayjs(item.createdAt).format("MMM DD, HH:mm:ss")} + + + {dayjs(item.createdAt).fromNow()} + + + - - {data?.data?.list?.length ? ( - data.data.list.map((item) => { - const parsed = JSON.parse((item.data as any) || "{}"); - - return ( - - - {/* Nama & Nomor Pengirim */} - - - - {parsed.name || "Unknown Sender"} ({parsed.number || "No Number"}) + + + + + Inbound Message + {parsed.question || "(Empty message)"} + - {/* Pertanyaan / Pesan */} - - - - {parsed.question || "(No question)"} - - - - {/* ID Record */} - - - - {item.id} - - - - {/* Timestamp */} - - - - {dayjs(item.createdAt).format("YYYY-MM-DD HH:mm:ss")} - - - - {/* Flow ID */} - {parsed.flowId && ( - - - - Flow: {parsed.flowId} - + {parsed.answer && ( + + + + + + AI Response + + + {parsed.flowId && ( + + Flow: {parsed.flowId} + + )} - )} + {parsed.answer} + + )} - {/* Jawaban */} - {parsed.answer && ( - - - - Bot Answer - - - {parsed.answer} - - - - )} - - - ); - }) - ) : ( - - - No webhook activity detected yet. + + + + + ID: {item.id} + + + + + + ); + }) + ) : ( +
+ + + + No hook activity detected yet. - - )} - - - - - { - setPage(value); - mutate(); - }} - radius="xl" - withEdges - color="teal" - size="md" - styles={{ - control: { - backgroundColor: "#2D2D2D", - border: "1px solid rgba(0,255,200,0.15)", - color: "#EAEAEA", - }, - }} - /> - + +
+ )}
-
+ + + { + setPage(value); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + color="teal" + size="sm" + radius="md" + withEdges + /> + +
); -} +} \ No newline at end of file diff --git a/src/pages/sq/dashboard/wa-hook/wa_hook_layout.tsx b/src/pages/sq/dashboard/wa-hook/wa_hook_layout.tsx index de154b6..0842749 100644 --- a/src/pages/sq/dashboard/wa-hook/wa_hook_layout.tsx +++ b/src/pages/sq/dashboard/wa-hook/wa_hook_layout.tsx @@ -7,7 +7,7 @@ export default function WaHookLayout() { return ( - + */} diff --git a/src/pages/sq/dashboard/wajs/wajs_home.tsx b/src/pages/sq/dashboard/wajs/wajs_home.tsx index ef25eae..92f8f8f 100644 --- a/src/pages/sq/dashboard/wajs/wajs_home.tsx +++ b/src/pages/sq/dashboard/wajs/wajs_home.tsx @@ -1,3 +1,326 @@ +import clientRoutes from "@/clientRoutes"; +import apiFetch from "@/lib/apiFetch"; +import { + Badge, + Box, + Button, + Center, + Divider, + Grid, + Group, + Loader, + Paper, + SimpleGrid, + Stack, + Text, + ThemeIcon, + Title, + TextInput, + Textarea, + rem, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { notifications } from "@mantine/notifications"; +import { + IconArrowRight, + IconBolt, + IconChevronRight, + IconClock, + IconKey, + IconMessage2, + IconWebhook, + IconSend, + IconPhone, +} from "@tabler/icons-react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { useState } from "react"; +import { Link } from "react-router-dom"; +import useSWR from "swr"; + +dayjs.extend(relativeTime); + export default function WajsHome() { - return

Wajs Home

; + const [sending, setSending] = useState(false); + const { data: hookData, isLoading: hooksLoading } = useSWR( + "/wa-hook/list?limit=5", + () => apiFetch["wa-hook"].list.get({ query: { limit: 5 } }) + ); + + const form = useForm({ + initialValues: { + number: "", + text: "", + }, + validate: { + number: (value) => (value.length < 5 ? "Invalid phone number" : null), + text: (value) => (value.length < 1 ? "Message cannot be empty" : null), + }, + }); + + const handleSendMessage = async (values: typeof form.values) => { + setSending(true); + try { + const { data, error } = await apiFetch.api.wa["send-text"].post(values); + if (error) { + notifications.show({ + title: "Failed to send", + message: (error as any)?.value?.message || "Something went wrong", + color: "red", + }); + } else { + notifications.show({ + title: "Message Sent", + message: `Successfully sent to ${values.number}`, + color: "teal", + icon: , + }); + form.reset(); + } + } catch (err) { + notifications.show({ + title: "Error", + message: "Network error or server unreachable", + color: "red", + }); + } finally { + setSending(false); + } + }; + + const stats = [ + { + title: "Incoming Messages", + value: hookData?.data && 'count' in hookData.data ? (hookData.data.count as number) * 10 : "...", + icon: IconMessage2, + color: "blue", + }, + { + title: "Active Webhooks", + value: "8", + icon: IconWebhook, + color: "teal", + }, + { + title: "API Keys", + value: "3", + icon: IconKey, + color: "violet", + }, + ]; + + const recentHooks = (hookData?.data && 'list' in hookData.data ? hookData.data.list : []) as any[]; + + return ( + + + + Dashboard Overview + + + Monitor your WhatsApp integration activity and system health. + + + + + {stats.map((stat) => ( + + + + + {stat.title} + + + {stat.value} + + + + + + + + ))} + + + + + + + + + + + + Recent Hook Activity + + + + + + + + {hooksLoading ? ( +
+ +
+ ) : recentHooks.length === 0 ? ( +
+ + No recent activity found. + +
+ ) : ( + recentHooks.map((hook: any) => ( + ({ + borderRadius: theme.radius.sm, + transition: "background-color 100ms ease", + "&:hover": { + backgroundColor: "var(--mantine-color-default-hover)", + }, + })} + > + + + + {hook.data?.type || "MESSAGE"} + + + + {hook.data?.text || "Media message received"} + + + From: {hook.data?.number || "Unknown"} + + + + + {dayjs(hook.createdAt).fromNow()} + + + + )) + )} +
+
+ + + + + + + Send Test Message + +
+ + } + {...form.getInputProps("number")} + /> +