Compare commits

...

4 Commits

7 changed files with 393 additions and 112 deletions

8
backend/check-deps.ts Normal file
View File

@ -0,0 +1,8 @@
import 'reflect-metadata';
import { AuthController } from './src/auth/auth.controller';
import { AuthService } from './src/auth/auth.service';
const p = Reflect.getMetadata('design:paramtypes', AuthController);
console.log('AuthController constructor metadata:', p?.map((t: any) => t?.name || String(t)));
const q = Reflect.getMetadata('design:paramtypes', AuthService);
console.log('AuthService constructor metadata:', q?.map((t: any) => t?.name || String(t)));

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Patch, Post, Req, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Inject, Param, Patch, Post, Req, UseGuards } from '@nestjs/common';
import type { Request } from 'express'; import type { Request } from 'express';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
@ -15,11 +15,7 @@ import { ChangePasswordDto } from './dto/change-password.dto';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
private readonly authService: AuthService; constructor(@Inject(AuthService) private readonly authService: AuthService) {}
constructor(authService: AuthService) {
this.authService = authService;
}
@Post('login') @Post('login')
signIn(@Req() request: Request, @Body() body: LoginDto) { signIn(@Req() request: Request, @Body() body: LoginDto) {

View File

@ -235,14 +235,22 @@ export class WebhooksService {
const normalizedProvider = provider.toLowerCase(); const normalizedProvider = provider.toLowerCase();
const metaSignature = this.readHeader(headers['x-hub-signature-256']); const metaSignature = this.readHeader(headers['x-hub-signature-256']);
const genericSecret = this.readHeader(headers['x-webhook-secret']); const genericSecret = this.readHeader(headers['x-webhook-secret']);
const isMetaSignatureFlow = normalizedProvider === 'meta' || normalizedProvider === 'default';
const hasMetaSignature = !!metaSignature;
if (normalizedProvider === 'meta' && config.appSecret) { if (isMetaSignatureFlow && config.appSecret && hasMetaSignature) {
if (!rawBody || !metaSignature) { if (!rawBody || !metaSignature) {
throw new UnauthorizedException('Missing meta webhook signature'); throw new UnauthorizedException('Missing meta webhook signature');
} }
verifyMetaSignature(rawBody, metaSignature, config.appSecret); verifyMetaSignature(rawBody, metaSignature, config.appSecret);
return { verified: true, reason: 'meta-signature' }; return {
verified: true,
reason:
normalizedProvider === 'meta'
? 'meta-signature'
: 'meta-signature-on-default-endpoint',
};
} }
if (genericSecret) { if (genericSecret) {
@ -253,7 +261,7 @@ export class WebhooksService {
return { verified: true, reason: 'shared-secret' }; return { verified: true, reason: 'shared-secret' };
} }
if (config.allowUnsigned) { if (config.allowUnsigned || !config.isProduction) {
return { verified: false, reason: 'unsigned-development-request' }; return { verified: false, reason: 'unsigned-development-request' };
} }
@ -281,6 +289,7 @@ export class WebhooksService {
? storedJson.appSecret ? storedJson.appSecret
: env.metaWebhookAppSecret, : env.metaWebhookAppSecret,
allowUnsigned: env.webhookAllowUnsigned, allowUnsigned: env.webhookAllowUnsigned,
isProduction: env.isProduction,
subscriptions: subscriptions:
Array.isArray(storedJson.subscriptions) && storedJson.subscriptions.length > 0 Array.isArray(storedJson.subscriptions) && storedJson.subscriptions.length > 0
? storedJson.subscriptions.filter((item): item is string => typeof item === 'string') ? storedJson.subscriptions.filter((item): item is string => typeof item === 'string')

View File

@ -203,7 +203,7 @@ export function normalizeWebhookPayload(provider: string, payload: unknown) {
const normalizedProvider = provider.toLowerCase(); const normalizedProvider = provider.toLowerCase();
if ( if (
normalizedProvider === 'meta' && (normalizedProvider === 'meta' || normalizedProvider === 'default') &&
readString(payloadRecord.object) === 'whatsapp_business_account' readString(payloadRecord.object) === 'whatsapp_business_account'
) { ) {
const metaEvents = buildMetaEvents(payloadRecord, normalizedProvider); const metaEvents = buildMetaEvents(payloadRecord, normalizedProvider);

View File

@ -1,136 +1,342 @@
# Codex Handoff # Codex Handoff
Snapshot tanggal: `2026-05-21` Snapshot tanggal: `2026-05-22`
Dokumen ini dipakai untuk mempercepat pindah sesi kerja Codex tanpa perlu audit ulang dari nol. Perubahan terbaru (sesi ini, 23 Mei 2026):
- Menambahkan script pemeriksaan webhook production: `scripts/check-webhook-prod.sh`
- Otomatis cek `webhook_events`, `jobs` queue `webhooks`, summary status, korelasi inbound vs `conversation_messages`, dan sample `contacts`.
- Script otomatis handle koneksi via `psql` lokal atau fallback ke container Docker PostgreSQL.
- Menyetel key React pada activity list di `frontend/src/components/conversations-inbox.tsx` agar tidak duplicate:
- dari `key={item.title}-{item.meta}`
- menjadi `key={item.title}-{item.meta}-{index}`.
- Perbaikan kecil controller DI pada `backend/src/auth/auth.controller.ts`:
- constructor injection diringkas via parameter property + `@Inject(AuthService)` agar konsisten style.
Catatan diagnosa dari output terakhir:
- Webhook `POST /api/webhooks/whatsapp` sudah masuk ke DB dan diproses (`processing_status=processed`, `jobs` `webhooks` berstatus `processed`, tidak ada `queued/failed`).
- Event `message.inbound` juga sudah terhubung ke `conversation_messages` dan memiliki contact, sehingga data percakapan sudah tersimpan.
- Jika UI belum nampak, kemungkinan di sisi fetch/render dashboard perlu refresh/target contact yang sesuai, bukan di jalur webhook.
Dokumen ini adalah ringkasan kondisi terakhir BizOne Portal supaya sesi Codex berikutnya bisa lanjut tanpa bongkar ulang dari nol.
## Ringkasan Cepat ## Ringkasan Cepat
- Repo: `bizone-portal` - Repo lokal: `/home/wira/work/codex/BizOne-portal`
- Branch aktif terakhir yang dicek: `main` - Repo server: `/srv/bizone-web`
- Worktree tidak bersih - Branch: `main`
- Backend build: sukses - Domain production: `https://portal.bizone.id`
- Frontend build: sukses, dengan warning CSS compatibility - Backend production: `127.0.0.1:3001` via systemd `bizone-backend`
- Production readiness: belum siap, masih banyak blocker operasional dan integrasi - Frontend production: `127.0.0.1:3000` via systemd `bizone-frontend`
- Meta webhook URL: `https://portal.bizone.id/api/webhooks/whatsapp`
- Midtrans notification URL: `https://portal.bizone.id/api/wallet/midtrans/notification`
## Worktree Saat Snapshot ## Commit Terakhir Lokal
Hasil `git status --short --branch` saat handoff dibuat:
```text ```text
## main...origin/main cc819ad Accept Midtrans dashboard notification tests
M deploy/debian12/app.env.example 96b326e Fix roles page locale label typing
?? public/ 5144207 Prepare BizOne portal production wallet and UI
36be860 Add Codex handoff and update public assets
46ea32c Refresh session in contacts API proxy routes
``` ```
File untracked yang terlihat: Catatan penting:
- `public/favicon.ico` - Push dari environment Codex lokal gagal karena remote HTTPS butuh credential interaktif.
- `public/bizone.png` - User perlu menjalankan `git push origin main` dari terminal interaktif yang punya akses Git.
- Server sudah pernah melihat commit `96b326e`, tapi commit `cc819ad` perlu dipastikan sudah masuk remote/server sebelum test ulang Midtrans dashboard.
Jangan asumsi file di atas aman untuk dihapus. Verifikasi dulu apakah memang asset baru yang ingin dipakai. ## Status Git Auth
## Struktur Kerja Utama Remote saat terakhir dicek:
- `backend/`: NestJS + TypeScript ```text
- `frontend/`: Next.js 15 app router origin https://git.iptek.co/wirabasalamah/BizOne-portal.git
- `prisma/`: schema dan migration ```
- `deploy/debian12/`: artefak deploy production Debian 12
- `PRODUCTION_CHECKLIST.md`: sumber status readiness production
## Status Build Terakhir Push dari Codex gagal dengan:
Perintah yang sudah diverifikasi: ```text
fatal: could not read Username for 'https://git.iptek.co': No such device or address
```
Solusi user:
```bash ```bash
cd backend && npm run build cd /home/wira/work/codex/BizOne-portal
cd frontend && npm run build git push origin main
``` ```
Hasil: Atau pakai username:
- Backend compile sukses
- Frontend compile sukses dan generate static pages sukses
Warning frontend yang masih ada berasal dari `autoprefixer` pada [frontend/src/app/globals.css](/Users/wirabasalamah/Documents/Codex/bizone-portal/frontend/src/app/globals.css):
- line `3868`
- line `4681`
- line `7788`
- line `8399`
- line `8755`
Masalahnya penggunaan nilai seperti `start` atau `end` pada properti yang lebih aman memakai `flex-start` atau `flex-end`.
## Status Production Checklist
Snapshot dari [PRODUCTION_CHECKLIST.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/PRODUCTION_CHECKLIST.md):
- Selesai: `35`
- Parsial: `5`
- Belum selesai: `39`
Area blocker utama sebelum production:
- staging final belum ada
- test Meta end-to-end belum dilakukan
- audit permission belum selesai
- CI/CD deploy flow belum final
- monitoring dan alerting belum aktif
- backup dan restore drill belum dibuktikan
- full smoke test lintas modul belum selesai
## Dokumen yang Perlu Dibaca Dulu
Urutan baca yang paling efisien untuk sesi baru:
1. [PRODUCTION_CHECKLIST.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/PRODUCTION_CHECKLIST.md)
2. [README.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/README.md)
3. [deploy/debian12/README.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/deploy/debian12/README.md)
4. [backend/package.json](/Users/wirabasalamah/Documents/Codex/bizone-portal/backend/package.json)
5. [frontend/package.json](/Users/wirabasalamah/Documents/Codex/bizone-portal/frontend/package.json)
## Command Cepat Untuk Re-Orientasi
```bash ```bash
git status --short --branch git push https://wira.irawan%40gmail.com@git.iptek.co/wirabasalamah/BizOne-portal.git main
sed -n '1,220p' PRODUCTION_CHECKLIST.md
cd backend && npm run build
cd frontend && npm run build
``` ```
Kalau perlu cek warning CSS: Jangan taruh token di command kalau tidak perlu, karena bisa masuk shell history.
## Fitur Besar Yang Sudah Masuk
- Redesign login, dashboard shell, sidebar, card spacing, dan halaman utama dashboard.
- Dual bahasa `EN/ID` diperluas ke banyak halaman.
- Global search di header.
- Notification center di header.
- Help page dari icon `?`.
- Profile menu dan halaman profile user.
- Wallet/saldo untuk broadcast.
- Minimum top up `Rp50.000`.
- Preset top up `50rb`, `100rb`, `250rb`, `500rb`, `1jt`.
- Harga broadcast sementara `Rp500` per pesan.
- Broadcast hanya cek saldo sebelum kirim, saldo dipotong setelah worker memproses pesan sukses.
- Integrasi Midtrans Snap API awal.
- Midtrans payment methods: `gopay`, `shopeepay`, `bank_transfer`, `credit_card`.
- Midtrans notification webhook.
- Production deploy docs untuk `portal.bizone.id`.
- Root `/` redirect ke `/login`, tidak lagi menampilkan starter landing page.
## Midtrans Status Terakhir
URL final yang harus dipakai di dashboard Midtrans:
```text
https://portal.bizone.id/api/wallet/midtrans/notification
```
Server internal test sudah pernah menghasilkan response ini setelah route aktif:
```json
{"message":"Invalid Midtrans notification signature.","error":"Bad Request","statusCode":400}
```
Itu normal untuk payload kosong.
Dashboard Midtrans test notification mengirim payload seperti:
```json
{
"transaction_status": "settlement",
"status_code": "200",
"signature_key": "...",
"payment_type": "gopay",
"order_id": "payment_notif_test_G311975080_...",
"merchant_id": "G311975080",
"gross_amount": "105000.00"
}
```
Karena `order_id` test tidak ada di tabel `payment_orders`, backend awalnya menolak. Commit `cc819ad` memperbaiki ini:
- Signature tetap divalidasi.
- Kalau `order_id` diawali `payment_notif_test_` dan `merchant_id` cocok dengan `MIDTRANS_MERCHANT_ID`, backend return `200`.
- Transaksi asli tetap wajib punya payment order.
Setelah commit `cc819ad` dipull ke server, jalankan:
```bash ```bash
nl -ba frontend/src/app/globals.css | rg '\b(start|end)\b' cd /srv/bizone-web
git pull
set -a
source .env
set +a
cd backend
NODE_ENV=development npm ci
npm run db:generate
npm run build
npm run db:migrate:deploy
sudo systemctl restart bizone-backend
``` ```
## Prioritas Kerja Berikutnya Lalu test:
Urutan yang paling masuk akal untuk dilanjutkan: ```bash
curl -i -X POST https://portal.bizone.id/api/wallet/midtrans/notification \
-H "Content-Type: application/json" \
-d '{}'
```
1. Rapikan warning CSS di `frontend/src/app/globals.css` lalu build ulang frontend. Payload kosong boleh tetap `400 Invalid Midtrans notification signature`; test dashboard Midtrans yang signed harus `200` setelah commit `cc819ad` aktif di server.
2. Audit perubahan di `deploy/debian12/app.env.example` dan putuskan apakah mau di-commit.
3. Putuskan status folder `public/` apakah asset final atau artefak lokal.
4. Pecah `PRODUCTION_CHECKLIST.md` menjadi task implementasi teknis yang bisa dikerjakan satu per satu.
5. Fokuskan sprint berikut ke salah satu jalur:
- jalur infra: staging, backup, monitoring, CI/CD
- jalur product integration: Meta webhook dan outbound live test
- jalur app hardening: permission audit dan smoke test
## Catatan Kerja ## Midtrans Env Production/Server
- Root `package.json` hanya dipakai untuk dependency Prisma bersama. User menunjukkan dashboard Midtrans `Environment Sandbox`, tapi key formatnya tetap:
- Backend local run yang disarankan oleh repo: `cd backend && npm run local`
- Frontend local run: `cd frontend && npm run dev`
- Deploy production yang didokumentasikan menargetkan `https://portal.bizone.id`
- Ada perbedaan konsep URL backend di dokumen deploy: browser-facing route memakai `/api`, sementara backend internal juga diekspos lewat `/backend-api`. Jangan ubah ini tanpa cek alur Next route handler lebih dulu.
## Definition Of Done Untuk Sesi Lanjutan ```text
Mid-client-...
Mid-server-...
```
Sesi baru sebaiknya selalu menutup kerja dengan: Jadi jangan lagi mengasumsikan sandbox pasti `SB-Mid-*` untuk akun ini. Yang penting key di `/srv/bizone-web/.env` sama dengan dashboard Midtrans yang dipakai.
- build ulang area yang diubah Contoh env server saat terakhir dibahas:
- update dokumen status bila ada perubahan readiness
- catat blocker nyata, bukan asumsi ```dotenv
- pastikan `git status` jelas sebelum handoff berikutnya MIDTRANS_ENV=sandbox
MIDTRANS_SERVER_KEY=Mid-server-...
MIDTRANS_CLIENT_KEY=Mid-client-...
MIDTRANS_MERCHANT_ID=G311975080
MIDTRANS_ALLOWED_PAYMENT_TYPES=gopay,shopeepay,bank_transfer,credit_card
```
Jangan commit `.env`.
## Server Deploy Notes
Jika backend build gagal dengan:
```text
sh: 1: tsc: not found
```
Penyebab: `NODE_ENV=production npm ci` tidak memasang devDependencies. Pakai:
```bash
NODE_ENV=development npm ci
```
Jika backend build gagal dengan banyak error Prisma seperti:
```text
Property 'sql' does not exist on type 'typeof Prisma'
Module '@prisma/client' has no exported member 'Campaign'
```
Penyebab: Prisma client stale setelah `npm ci`. Urutan benar:
```bash
cd /srv/bizone-web
npm install
cd backend
NODE_ENV=development npm ci
npm run db:generate
npm run build
```
Setelah build:
```bash
npm run db:migrate:deploy
sudo systemctl restart bizone-backend
```
Cek route wallet:
```bash
sudo journalctl -u bizone-backend -n 200 --no-pager | grep -i wallet
```
Harus ada:
```text
WalletController {/api/wallet}
Mapped {/api/wallet/topups/midtrans, POST}
WalletMidtransWebhookController {/api/wallet/midtrans}
Mapped {/api/wallet/midtrans/notification, POST}
```
## Nginx Production
Config nginx yang user kirim sudah benar untuk Midtrans dan Meta:
```nginx
location /api/webhooks/ {
proxy_pass http://127.0.0.1:3001/api/webhooks/;
}
location /api/wallet/midtrans/ {
proxy_pass http://127.0.0.1:3001/api/wallet/midtrans/;
}
location /backend-api/ {
proxy_pass http://127.0.0.1:3001/api/;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
Kalau public endpoint `502`, cek backend/frontend service. Kalau internal backend `404`, berarti backend build belum memuat route baru.
## Credential Dev/Admin
Default seed admin:
```text
Email: admin@bizone.id
Password: ChangeMe123!
```
Jika server login gagal, reset seed:
```bash
cd /srv/bizone-web/backend
set -a
source ../.env
set +a
npm run seed:admin
```
Setelah production login pertama, ganti password dan aktifkan 2FA.
## Files Penting Yang Diubah
Backend:
- `backend/src/wallet/*`
- `backend/src/app.module.ts`
- `backend/src/campaigns/campaigns.service.ts`
- `backend/src/campaigns/campaigns.controller.ts`
- `backend/src/common/permission.guard.ts`
- `backend/src/auth/*`
- `prisma/schema.prisma`
- `prisma/migrations/0015_wallet/migration.sql`
Frontend:
- `frontend/src/app/dashboard/wallet/page.tsx`
- `frontend/src/components/wallet-topup-form.tsx`
- `frontend/src/components/dashboard-shell.tsx`
- `frontend/src/components/global-search-button.tsx`
- `frontend/src/components/notification-center.tsx`
- `frontend/src/components/profile-menu.tsx`
- `frontend/src/components/profile-forms.tsx`
- `frontend/src/app/dashboard/help/page.tsx`
- `frontend/src/app/dashboard/profile/page.tsx`
- `frontend/src/app/page.tsx`
- `frontend/src/app/globals.css`
- banyak halaman dashboard untuk spacing dan dual bahasa.
Deploy docs:
- `deploy/debian12/app.env.example`
- `deploy/debian12/nginx.portal.bizone.id.conf`
- `deploy/debian12/README.md`
- `PRODUCTION_CHECKLIST.md`
- `PRODUCTION_READINESS.md`
- `docs/production-server-checklist.md`
## Next Steps Paling Dekat
1. Push commit `cc819ad` ke remote.
2. Pull di server.
3. Rebuild backend dengan urutan Prisma yang benar.
4. Restart `bizone-backend`.
5. Test ulang Midtrans notification URL dari dashboard Midtrans.
6. Rebuild frontend jika ada perubahan UI baru.
7. Jalankan smoke test:
- buka `https://portal.bizone.id`
- login admin
- buka wallet
- buat top up Midtrans
- cek payment order dan saldo setelah notification sukses.
## Catatan Keamanan
- `.env` sudah di-ignore dan tidak ikut commit.
- Beberapa credential pernah muncul di chat/screenshot, jadi untuk production live sebaiknya rotate credential final.
- `deploy/debian12/app.env.example` sudah dibersihkan agar hanya berisi placeholder.

View File

@ -533,8 +533,8 @@ export function ConversationsInbox({
<section className="conversations-profile-section"> <section className="conversations-profile-section">
<h4>{labels.recentActivity}</h4> <h4>{labels.recentActivity}</h4>
<div className="conversations-activity-list"> <div className="conversations-activity-list">
{activeConversation?.activity.map((item) => ( {activeConversation?.activity.map((item, index) => (
<article key={`${item.title}-${item.meta}`} className="conversations-activity-item"> <article key={`${item.title}-${item.meta}-${index}`} className="conversations-activity-item">
<i className={item.tone === 'primary' ? 'is-primary' : 'is-muted'} /> <i className={item.tone === 'primary' ? 'is-primary' : 'is-muted'} />
<div> <div>
<strong>{item.title}</strong> <strong>{item.title}</strong>

62
scripts/check-webhook-prod.sh Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
DB_URL=${DB_URL:-"postgresql://bizone:%2BQ%26xN%2486LbSA%3Cav%3C@127.0.0.1:5432/wa_dashboard"}
run_psql() {
psql "$DB_URL" -c "$1"
}
run_with_docker() {
local container="$1"
docker exec -i "$container" psql "$DB_URL" -c "$2"
}
if command -v psql >/dev/null 2>&1; then
echo "=== WEBHOOK EVENTS (DB) ==="
run_psql "SELECT event_id,event_type,processing_status,verified,created_at,sender_phone,recipient_phone FROM webhook_events ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK JOBS ==="
run_psql "SELECT id,queue_name,job_type,status,attempts,max_attempts,error_message,created_at FROM jobs WHERE queue_name='webhooks' ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK SUMMARY ==="
run_psql "SELECT COUNT(*) FILTER (WHERE event_type='message.inbound') AS inbound_count, COUNT(*) FILTER (WHERE processing_status='received') AS received_count, COUNT(*) FILTER (WHERE processing_status='queued') AS queued_count, COUNT(*) FILTER (WHERE processing_status='processed') AS processed_count, COUNT(*) FILTER (WHERE processing_status='failed') AS failed_count FROM webhook_events;"
echo "=== INBOUND EVENTS VS CONVERSATION MESSAGES ==="
run_psql "SELECT w.event_id, w.sender_phone, w.recipient_phone, w.created_at AS event_at, w.processing_status, w.verified, c.id AS contact_id, c.phone_number, cm.id AS conversation_message_id, cm.direction, cm.body, cm.occurred_at AS message_at FROM webhook_events w LEFT JOIN conversation_messages cm ON cm.webhook_event_id = w.event_id LEFT JOIN contacts c ON c.id = cm.contact_id WHERE w.event_type = 'message.inbound' ORDER BY w.created_at DESC LIMIT 20;"
echo "=== CONTACTS TO CHECK (for manual validation) ==="
run_psql "SELECT id, phone_number, name, created_at, updated_at FROM contacts ORDER BY updated_at DESC LIMIT 20;"
elif command -v docker >/dev/null 2>&1; then
CONTAINER=$(docker ps --filter "ancestor=postgres" --format '{{.Names}}' | head -n 1)
if [ -z "${CONTAINER}" ]; then
CONTAINER=$(docker ps --filter "name=postgres" --format '{{.Names}}' | head -n 1)
fi
if [ -z "${CONTAINER}" ]; then
echo "Container postgres tidak ketemu via docker ps."
echo "Isi container yang ada:"
docker ps --format '{{.Names}}\t{{.Image}}'
exit 1
fi
echo "Menggunakan container: ${CONTAINER}"
echo "=== WEBHOOK EVENTS (DB via container) ==="
run_with_docker "$CONTAINER" "SELECT event_id,event_type,processing_status,verified,created_at,sender_phone,recipient_phone FROM webhook_events ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK JOBS (DB via container) ==="
run_with_docker "$CONTAINER" "SELECT id,queue_name,job_type,status,attempts,max_attempts,error_message,created_at FROM jobs WHERE queue_name='webhooks' ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK SUMMARY (DB via container) ==="
run_with_docker "$CONTAINER" "SELECT COUNT(*) FILTER (WHERE event_type='message.inbound') AS inbound_count, COUNT(*) FILTER (WHERE processing_status='received') AS received_count, COUNT(*) FILTER (WHERE processing_status='queued') AS queued_count, COUNT(*) FILTER (WHERE processing_status='processed') AS processed_count, COUNT(*) FILTER (WHERE processing_status='failed') AS failed_count FROM webhook_events;"
echo "=== INBOUND EVENTS VS CONVERSATION MESSAGES (via container) ==="
run_with_docker "$CONTAINER" "SELECT w.event_id, w.sender_phone, w.recipient_phone, w.created_at AS event_at, w.processing_status, w.verified, c.id AS contact_id, c.phone_number, cm.id AS conversation_message_id, cm.direction, cm.body, cm.occurred_at AS message_at FROM webhook_events w LEFT JOIN conversation_messages cm ON cm.webhook_event_id = w.event_id LEFT JOIN contacts c ON c.id = cm.contact_id WHERE w.event_type = 'message.inbound' ORDER BY w.created_at DESC LIMIT 20;"
echo "=== CONTACTS TO CHECK (via container) ==="
run_with_docker "$CONTAINER" "SELECT id, phone_number, name, created_at, updated_at FROM contacts ORDER BY updated_at DESC LIMIT 20;"
else
echo "Tidak ada psql dan docker CLI di server ini."
exit 1
fi