60 Commits

Author SHA1 Message Date
ee543a16ad Merge pull request 'amalia/30-apr-26' (#17) from amalia/30-apr-26 into main
Reviewed-on: #17
2026-04-30 17:27:11 +08:00
87ffc4ac7d chore: bump version to 0.1.2 2026-04-30 15:21:28 +08:00
722bca8a61 refactor: replace global API_KEY with per-app clientApiKey
Remove global API_KEY env var and its validation in checkAuth.
Auth via x-api-key now exclusively uses clientApiKey per-app
validated inline on POST /api/bugs.
2026-04-30 15:16:54 +08:00
6124ee5bf6 fix: show today timestamp as single line on activity logs 2026-04-30 14:57:27 +08:00
40a5f38eaf feat: add clientApiKey per-app for mobile bug submission 2026-04-30 13:50:29 +08:00
4e9d5964ae feat: merge url_api & api_key to App, add application settings page 2026-04-30 11:28:25 +08:00
e2ad6f9313 Merge pull request 'amalia/29-apr-26' (#16) from amalia/29-apr-26 into main
Reviewed-on: #16
2026-04-29 17:40:58 +08:00
83e8becaa3 upd: mcp json 2026-04-29 17:39:43 +08:00
a2c7be7cfa chore: bump version to 0.1.1 2026-04-29 16:38:19 +08:00
f44a8216bf fix: jalankan prisma migrate deploy otomatis jika ada pending migrations
Sebelumnya pipeline dibatalkan saat ada pending migrations.
Sekarang langsung deploy migrations lalu lanjut ke step berikutnya.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 16:26:35 +08:00
7609204a13 feat: tambah deploy pipeline tool di MCP deploy-stg
Tool baru `deploy` menjalankan full pipeline:
1. Cek pending migrations → batalkan jika ada
2. Version bump package.json ke tag baru
3. Commit + push ke build/stg
4. Trigger publish.yml → polling hingga selesai
5. Trigger re-pull.yml → polling hingga selesai
6. Cek version di STG_URL vs local untuk konfirmasi

Env baru: STG_URL (staging app URL), VERSION_PATH (default /api/system/version)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 16:12:22 +08:00
d3a4f97d0e feat: tambah MCP server deploy-stg untuk trigger GitHub workflow
- scripts/mcp-deploy.ts: MCP server dengan 2 tool:
  - publish: trigger publish.yml (build & push image stg)
  - repull: trigger re-pull.yml (redeploy stack di Portainer)
- .mcp.json: registrasi server dengan env GH_TOKEN, STACK_NAME, BASE_URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 16:01:16 +08:00
5050835d81 upd: tambah notifikasi success/error saat simpan Settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:54:38 +08:00
d17b49cf8f upd: gabungkan Settings menjadi 1 form dengan tombol Simpan Semua
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:51:51 +08:00
8bcb30a85b feat: tambah API Key Desa Plus ke Settings panel dan proxy
- Tambah kolom API_KEY_DESA_PLUS di CONFIG_DEFINITIONS (ditampilkan sebagai password field)
- Proxy otomatis menyertakan X-API-Key header jika API key sudah dikonfigurasi

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:51:09 +08:00
ccc43e0c96 feat: runtime config via DB — ganti VITE_URL_API_DESA_PLUS dengan proxy
- Tambah model AppConfig (key-value) ke schema + migration
- Tambah GET/PUT /api/admin/config (DEVELOPER only)
- Tambah proxy /api/proxy/desa-plus/* yang baca URL dari DB
- Hapus VITE_URL_API_DESA_PLUS dari frontend, ganti semua URL desa-plus ke relative proxy path
- Aktifkan Settings tab di /dev dengan UI untuk set URL_API_DESA_PLUS

URL desa-plus kini bisa diubah via /dev → Settings tanpa rebuild image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:47:21 +08:00
b63117694b feat: add /api/system/version endpoint with changelog
Mengembalikan versi aplikasi, git commit hash, branch aktif, dan 20 commit terakhir untuk memverifikasi apakah staging/production sudah terupdate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 13:56:23 +08:00
dbbe53584c upd: sync compose.yml and .env.example with all env vars in env.ts
Add missing required vars: API_KEY, MINIO_*, and optional REDIS_URL, BUN_PUBLIC_BASE_URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 12:01:44 +08:00
06794524fd feat: block inactive users from login and fix activity log on dev operators
- Block inactive users on email/password login (403)
- Block inactive users on Google OAuth (redirect to account_disabled)
- Auto-logout inactive users on session check (deleteMany sessions)
- Delete sessions when user is deactivated via PATCH /api/operators/:id
- Add account_disabled error message on login page
- Show inactive indicator on users table with reactivate button
- Add createSystemLog calls to /api/admin/users role and activate endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 11:58:31 +08:00
73aa9729b8 upd: remove Settings menu item from DashboardLayout user menu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 11:39:13 +08:00
7c5a491ba9 upd: update role management descriptions and add USER role card
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 11:33:39 +08:00
3c6fac1943 feat: redesign /logs page with table UI and date range filter
- Replace timeline view with table layout (Time, Operator, Type, Message)
- Add date range filter using @mantine/dates DatePickerInput
- Add SegmentedControl for log type filter
- Disable App Logs and Settings menu on /dev
- Remove Activity Logs menu from /dev (moved to /logs)
- Add dateFrom/dateTo query params to /api/logs backend
- Import @mantine/dates/styles.css to fix datepicker styling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 11:27:05 +08:00
6b6e3f3430 Merge pull request 'amalia/28-apr-26' (#15) from amalia/28-apr-26 into main
Reviewed-on: #15
2026-04-28 17:37:18 +08:00
b03f267743 upd: routing dev 2026-04-28 17:34:45 +08:00
94724a5081 feat: add Google OAuth login with USER role and pending approval flow
- Add GET /api/auth/google and GET /api/auth/callback/google routes with CSRF state protection and account linking via googleId
- Add getPublicOrigin() for dynamic redirect_uri (supports reverse proxy via X-Forwarded-Proto)
- Add USER role to schema (default for new Google sign-ins), make password optional, add googleId and image fields
- Role-based redirect after login: USER → /profile, ADMIN/DEVELOPER → /dashboard
- Profile page shows pending approval alert for USER role
- Dashboard redirects USER role back to profile
- Login page shows specific error messages per OAuth error code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:06:13 +08:00
373198c7c4 Merge pull request 'amalia/27-apr-26' (#14) from amalia/27-apr-26 into main
Reviewed-on: #14
2026-04-28 10:19:12 +08:00
9d80eb3b85 upd 2026-04-27 17:33:19 +08:00
8527671f46 upd:compose 2026-04-27 17:16:36 +08:00
1104217070 upd:dockerfile 2026-04-27 16:58:44 +08:00
786953054a upd:dockerfile 2026-04-27 16:53:10 +08:00
9c725fa230 Merge branch 'main' of https://github.com/bipprojectbali/monitoring-app into amalia/24-apr-26 2026-04-27 16:27:43 +08:00
github-actions[bot]
3ec6383535 chore: sync workflows from base-template 2026-04-27 08:22:51 +00:00
8f6a68b9f1 Merge pull request 'amalia/24-apr-26' (#13) from amalia/24-apr-26 into main
Reviewed-on: #13
2026-04-24 17:39:46 +08:00
63c0a6acff feat: image upload & preview untuk bug reports via MinIO
- Upload hingga 3 gambar per bug report (FileInput multi-select)
- Backend: POST /api/upload/image → MinIO, GET /api/bugs/images → presigned URL redirect
- Auto-create bucket jika belum ada saat server start
- Preview gambar fullscreen saat thumbnail diklik
- Diterapkan di /bug-reports dan /apps/$appId/errors
- Migrasi storage dari Seafile ke MinIO (minio SDK v8)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:36:32 +08:00
a0ca6be8e1 docs: split CLAUDE.md into focused reference files
Move architecture, testing, and dev tools detail into docs/ and
reference them via @path pointers to keep CLAUDE.md slim.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 15:46:18 +08:00
eb77aca715 Merge pull request 'upd: stack trace hide/show toggle and date format DD/MM/YYYY 24h' (#12) from amalia/22-apr-26 into main
Reviewed-on: #12
2026-04-22 17:32:17 +08:00
041d891a8d upd: stack trace hide/show toggle and date format DD/MM/YYYY 24h
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 17:25:50 +08:00
de5ec1af93 Merge pull request 'upd: swagger docs, api key auth, bug fixes' (#11) from amalia/21-apr-26 into main
Reviewed-on: #11
2026-04-21 17:33:17 +08:00
d09a702d64 upd: swagger docs, api key auth, bug fixes
- tambah Elysia Swagger di /docs dengan deskripsi lengkap semua endpoint
- tambah API key auth (X-API-Key) untuk klien eksternal di POST /api/bugs
- tambah normalisasi BugSource: SYSTEM/USER untuk eksternal, QC/SYSTEM/USER untuk dashboard
- perbaiki source schema jadi optional string agar tidak reject nilai unknown dari klien lama
- hapus field status dari form create bug (selalu OPEN)
- perbaiki typo desa_plus → appId di apps.$appId.errors.tsx
- tambah toggle hide/show stack trace di bug-reports.tsx dan apps.$appId.errors.tsx
- perbaiki grafik desa (width(-1)/height(-1)) dengan minWidth: 0 pada grid item
- perbaiki error &[data-active] inline style di DashboardLayout → pindah ke CSS class
- update CLAUDE.md dengan arsitektur lengkap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:30:07 +08:00
cd295abf2c Merge pull request 'amalia/16-apr-26' (#10) from amalia/16-apr-26 into main
Reviewed-on: #10
2026-04-16 14:10:26 +08:00
e6dfac1ffe upd: fix graphic data 2026-04-16 11:47:19 +08:00
6ce7c93106 upd: grafik 2026-04-16 10:19:51 +08:00
f446aec734 upd: role akses 2026-04-16 09:52:17 +08:00
08d67a304a Merge pull request 'amalia/15-apr-26' (#9) from amalia/15-apr-26 into main
Reviewed-on: #9
2026-04-15 15:25:55 +08:00
16ea551b4c upd: list app 2026-04-15 14:19:10 +08:00
c67fc9a230 upd: overview desa 2026-04-15 14:14:18 +08:00
c66ce4a39b upd: auth
Deskripsi:
-update login
- update struktur database

No Issues
2026-04-15 11:17:04 +08:00
840a89ea0a upd: qwen 2026-04-15 08:45:04 +08:00
2e51c01c7a Merge pull request 'amalia/14-apr-26' (#8) from amalia/14-apr-26 into main
Reviewed-on: #8
2026-04-14 17:23:38 +08:00
24fcc1ee76 upd: user staff
Deskripsi:
- connected to database pada halaman user
- tambah user
- delete user
- update user

No
Issues
2026-04-14 16:41:03 +08:00
f38081b1eb upd: menu dashboard
Deskripsi:
- connected to database

No Issues
2026-04-14 16:24:17 +08:00
a0cafbf2e2 upd: connected ke db
Deskripi:
- list error report general dan per apps
- update status
- update feedback

No Issues
2026-04-14 12:05:34 +08:00
14adaa8526 Merge pull request 'amalia/13-apr-26' (#7) from amalia/13-apr-26 into main
Reviewed-on: #7
2026-04-13 17:19:36 +08:00
65e9ed5ce6 upd: connected api 2026-04-13 17:15:41 +08:00
2cf96135f9 upd: menerapkan log pada semua aksi 2026-04-13 16:42:36 +08:00
14a9e2c687 upd: bug list
Deskripsi:
- tampilan list bug error
- tampilan tambah bug
- connected to database; list and create

No Issues
2026-04-13 15:17:46 +08:00
c0205ce2bf upd: user dan log activity 2026-04-13 14:48:49 +08:00
315ecc565e upd: api monitoring
Deskripsi :
- api deactivate or active desa
- api edit desa

No Issues
2026-04-13 11:21:25 +08:00
8c50768c98 upd: tampilan mode dark and light'; 2026-04-13 11:00:40 +08:00
5cc73d2290 Merge pull request 'upd: api monitoring user' (#6) from amalia/10-apr-26 into main
Reviewed-on: #6
2026-04-10 13:43:01 +08:00
81 changed files with 8565 additions and 917 deletions

View File

@@ -1,6 +1,7 @@
# App # App
PORT=3000 PORT=3000
NODE_ENV=development NODE_ENV=development
BUN_PUBLIC_BASE_URL=http://localhost:3000
# Dev Inspector # Dev Inspector
REACT_EDITOR=code REACT_EDITOR=code
@@ -13,9 +14,20 @@ DIRECT_URL=postgresql://user:password@localhost:5432/base-template
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
# Role # Super Admin (comma-separated emails)
SUPER_ADMIN_EMAIL=admin@example.com SUPER_ADMIN_EMAIL=admin@example.com
# Telegram Notification (optional) # API Key for external clients (e.g. mobile apps)
TELEGRAM_NOTIFY_TOKEN= API_KEY=your-secret-api-key-here
TELEGRAM_NOTIFY_CHAT_ID=
# MinIO (object storage for bug report images)
MINIO_ENDPOINT=
MINIO_PORT=443
MINIO_USE_SSL=true
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=
MINIO_UPLOAD_DIR=bug-reports
# Redis (optional — enables App Logs feature on /dev)
REDIS_URL=

106
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: Publish Docker to GHCR
on:
workflow_dispatch:
inputs:
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- prod
- stg
tag:
description: "Image tag (e.g. 1.0.0)"
required: true
default: "1.0.0"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h
- name: Checkout branch ${{ github.event.inputs.stack_env }}
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.stack_env }}
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
path: .ci
sparse-checkout: .github/workflows/script
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
type=raw,value=${{ github.event.inputs.stack_env }}-latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
no-cache: true
- name: Notify success
if: success()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
- name: Notify failure
if: failure()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"

60
.github/workflows/re-pull.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Re-Pull Docker
on:
workflow_dispatch:
inputs:
stack_name:
description: "stack name"
required: true
type: string
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- stg
- prod
jobs:
publish:
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
sparse-checkout: .github/workflows/script
- name: Deploy ke Portainer
run: bash ./.github/workflows/script/re-pull.sh
env:
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
- name: Notify success
if: success()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
- name: Notify failure
if: failure()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"

26
.github/workflows/script/notify.sh vendored Normal file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
if [ "$NOTIFY_STATUS" = "success" ]; then
ICON="✅"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
else
ICON="❌"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
fi
if [ -n "$NOTIFY_DETAIL" ]; then
TEXT="${TEXT}
${NOTIFY_DETAIL}"
fi
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg chat_id "$TELEGRAM_CHAT_ID" \
--arg text "$TEXT" \
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"

120
.github/workflows/script/re-pull.sh vendored Normal file
View File

@@ -0,0 +1,120 @@
#!/bin/bash
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
: "${STACK_NAME:?STACK_NAME tidak di-set}"
# Timeout total: MAX_RETRY * SLEEP_INTERVAL detik
MAX_RETRY=60 # 60 × 10s = 10 menit
SLEEP_INTERVAL=10
echo "🔐 Autentikasi ke Portainer..."
TOKEN=$(curl -s -X POST "https://${PORTAINER_URL}/api/auth" \
-H "Content-Type: application/json" \
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
| jq -r .jwt)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
exit 1
fi
echo "🔍 Mencari stack: $STACK_NAME..."
STACK=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks" \
-H "Authorization: Bearer ${TOKEN}" \
| jq ".[] | select(.Name == \"$STACK_NAME\")")
if [ -z "$STACK" ]; then
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
exit 1
fi
STACK_ID=$(echo "$STACK" | jq -r .Id)
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
ENV=$(echo "$STACK" | jq '.Env // []')
# ── Catat container ID lama sebelum redeploy ──────────────────────────────────
echo "📸 Mencatat container aktif sebelum redeploy..."
CONTAINERS_BEFORE=$(curl -s -X GET \
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
-H "Authorization: Bearer ${TOKEN}")
OLD_IDS=$(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Id] | join(",")')
echo " Container lama: $(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Names[0]] | join(", ")')"
# ── Ambil compose file lalu trigger redeploy ─────────────────────────────────
echo "📄 Mengambil compose file..."
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
-H "Authorization: Bearer ${TOKEN}" \
| jq -r .StackFileContent)
PAYLOAD=$(jq -n \
--arg content "$STACK_FILE" \
--argjson env "$ENV" \
'{stackFileContent: $content, env: $env, pullImage: true}')
echo "🚀 Triggering redeploy $STACK_NAME (pull latest image)..."
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
if [ "$HTTP_STATUS" != "200" ]; then
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
cat /tmp/portainer_response.json | jq .
exit 1
fi
echo "⏳ Menunggu image selesai di-pull dan container baru running..."
echo " (Timeout: $((MAX_RETRY * SLEEP_INTERVAL)) detik)"
COUNT=0
while [ $COUNT -lt $MAX_RETRY ]; do
sleep $SLEEP_INTERVAL
COUNT=$((COUNT + 1))
CONTAINERS=$(curl -s -X GET \
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
-H "Authorization: Bearer ${TOKEN}")
# Container baru = ID tidak ada di daftar container lama
NEW_RUNNING=$(echo "$CONTAINERS" | jq \
--arg old "$OLD_IDS" \
'[.[] | select(.State == "running" and ((.Id) as $id | ($old | split(",") | index($id)) == null))] | length')
FAILED=$(echo "$CONTAINERS" | jq \
'[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not))] | length')
echo "🔄 [$((COUNT * SLEEP_INTERVAL))s / $((MAX_RETRY * SLEEP_INTERVAL))s] Container baru running: ${NEW_RUNNING} | Gagal: ${FAILED}"
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status) | id: \(.Id[:12])"'
if [ "$FAILED" -gt "0" ]; then
echo ""
echo "❌ Ada container yang crash!"
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not)) | " → \(.Names[0]) | \(.Status)"'
exit 1
fi
if [ "$NEW_RUNNING" -gt "0" ]; then
# Cleanup dangling images setelah redeploy sukses
echo "🧹 Membersihkan dangling images..."
curl -s -X POST "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/images/prune" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filters":{"dangling":["true"]}}' | jq -r '" Reclaimed: \(.SpaceReclaimed // 0 | . / 1073741824 | tostring | .[0:5]) GB"'
echo "✅ Cleanup selesai!"
echo ""
echo "✅ Stack $STACK_NAME berhasil di-redeploy dengan image baru dan running!"
exit 0
fi
done
echo ""
echo "❌ Timeout $((MAX_RETRY * SLEEP_INTERVAL))s! Container baru tidak kunjung running."
echo " Kemungkinan image masih dalam proses pull atau ada error di server."
exit 1

16
.mcp.json Normal file
View File

@@ -0,0 +1,16 @@
{
"mcpServers": {
"deploy-stg": {
"type": "stdio",
"command": "bun",
"args": ["scripts/mcp-deploy.ts"],
"env": {
"GH_TOKEN": "${GH_TOKEN}",
"STACK_NAME": "monitoring-app",
"BASE_URL": "https://api.github.com/repos/bipprojectbali/monitoring-app",
"STG_URL": "https://monitoring-stg.wibudev.com",
"VERSION_PATH": "/api/system/version"
}
}
}
}

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

@@ -1,73 +1,52 @@
Default to using Bun instead of Node.js. # CLAUDE.md
- Use `bun <file>` instead of `node <file>` or `ts-node <file>` This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Use `bun test` instead of `jest` or `vitest`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## Server ## Runtime
Elysia.js as the HTTP framework, running on Bun. API routes are in `src/app.ts` (exported as `createApp()`), frontend serving and dev tools are in `src/index.tsx`. Default to Bun instead of Node.js everywhere:
- `src/app.ts` — Elysia app factory with all API routes (auth, hello, health, Google OAuth). Testable via `app.handle(request)`. - `bun <file>` not `node` / `ts-node`
- `src/index.tsx` — Server entry. Adds Vite middleware (dev) or static file serving (prod), click-to-source editor integration, and `.listen()`. - `bun test` not `jest` / `vitest`
- `src/serve.ts` — Dev entry (`bun --watch src/serve.ts`). Dynamic import workaround for Bun EADDRINUSE race. - `bun install` not `npm install` / `yarn` / `pnpm`
- `bun run <script>` not `npm run`
- `bunx <pkg>` not `npx`
- Bun auto-loads `.env` — never use dotenv.
## Database ## Common Commands
PostgreSQL via Prisma v6. Client generated to `./generated/prisma` (gitignored). ```bash
bun run dev # dev server with hot reload (bun --watch src/serve.ts)
bun run build # Vite production build
bun run start # production server (NODE_ENV=production)
bun run typecheck # tsc --noEmit
bun run lint # biome check src/
bun run lint:fix # biome check --write src/
- Schema: `prisma/schema.prisma` — User (id, name, email, password, timestamps) + Session (id, token, userId, expiresAt) # Database
- Client singleton: `src/lib/db.ts` — import `{ prisma }` from here bun run db:migrate # prisma migrate dev
- Seed: `prisma/seed.ts` — demo users with `Bun.password.hash` bcrypt bun run db:seed # seed demo data
- Commands: `bun run db:migrate`, `bun run db:seed`, `bun run db:generate` bun run db:generate # regenerate prisma client
bun run db:studio # Prisma Studio GUI
bun run db:push # push schema without migration
## Auth # Tests
bun run test # all tests
bun run test:unit # tests/unit/
bun run test:integration # tests/integration/ — no server needed
bun run test:e2e # tests/e2e/ — requires Lightpanda Docker
```
Session-based auth with HttpOnly cookies stored in DB. Run a single test file: `bun test tests/integration/auth.test.ts`
- Login: `POST /api/auth/login` — finds user by email, verifies password with `Bun.password.verify`, creates Session record ## Architecture
- Google OAuth: `GET /api/auth/google` → Google → `GET /api/auth/callback/google` — upserts user, creates session
- Session: `GET /api/auth/session` — looks up session by cookie token, returns user or 401, auto-deletes expired
- Logout: `POST /api/auth/logout` — deletes session from DB, clears cookie
## Frontend See @docs/ARCHITECTURE.md
React 19 + Vite 8 (middleware mode in dev). File-based routing with TanStack Router.
- Entry: `src/frontend.tsx` — renders App, removes splash screen, DevInspector in dev
- App: `src/frontend/App.tsx` — MantineProvider (dark, forced), QueryClientProvider, RouterProvider
- Routes: `src/frontend/routes/``__root.tsx`, `index.tsx`, `login.tsx`, `dashboard.tsx`
- Auth hooks: `src/frontend/hooks/useAuth.ts``useSession()`, `useLogin()`, `useLogout()`
- UI: Mantine v8 (dark theme `#242424`), react-icons
- Splash: `index.html` has inline dark CSS + spinner, removed on React mount
## Dev Tools
- Click-to-source: `Ctrl+Shift+Cmd+C` toggles inspector. Custom Vite plugin (`inspectorPlugin` in `src/vite.ts`) injects `data-inspector-*` attributes. Reads original file from disk for accurate line numbers.
- 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`.
## Testing ## Testing
Tests use `bun:test`. Three levels: See @docs/TESTING.md
```bash ## Dev Tools
bun run test # All tests
bun run test:unit # tests/unit/ — env, db connection, bcrypt
bun run test:integration # tests/integration/ — API endpoints via app.handle()
bun run test:e2e # tests/e2e/ — browser tests via Lightpanda CDP
```
- `tests/helpers.ts``createTestApp()`, `seedTestUser()`, `createTestSession()`, `cleanupTestData()` See @docs/DEV_TOOLS.md
- Integration tests use `createApp().handle(new Request(...))` — no server needed
- E2E tests use Lightpanda browser (Docker, `ws://127.0.0.1:9222`). App URLs use `host.docker.internal` from container. Lightpanda executes JS but POST fetch returns 407 — use integration tests for mutations.
## APIs
- `Bun.password.hash()` / `Bun.password.verify()` for bcrypt
- `Bun.file()` for static file serving in production
- `Bun.which()` / `Bun.spawn()` for editor integration
- `crypto.randomUUID()` for session tokens

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
FROM oven/bun:1 AS base
WORKDIR /app
# Install dependencies
FROM base AS deps
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Generate Prisma client
FROM deps AS prisma
COPY prisma ./prisma
RUN bunx prisma generate
# Build frontend (Vite → dist/)
FROM prisma AS builder
ARG VITE_URL_API_DESA_PLUS
ENV VITE_URL_API_DESA_PLUS=$VITE_URL_API_DESA_PLUS
COPY . .
RUN bun run build
# Runtime
FROM base AS runner
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/generated ./generated
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/src ./src
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
CMD ["bun", "src/index.tsx"]

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

357
bun.lock
View File

@@ -1,6 +1,5 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "bun-react-template", "name": "bun-react-template",
@@ -8,13 +7,22 @@
"@elysiajs/cors": "^1.4.1", "@elysiajs/cors": "^1.4.1",
"@elysiajs/eden": "^1.4.9", "@elysiajs/eden": "^1.4.9",
"@elysiajs/html": "^1.4.0", "@elysiajs/html": "^1.4.0",
"@mantine/charts": "^9.0.0", "@elysiajs/swagger": "^1.3.1",
"@mantine/charts": "^8.3.0",
"@mantine/core": "^8.3.18", "@mantine/core": "^8.3.18",
"@mantine/dates": "^8.3.0",
"@mantine/hooks": "^8.3.18", "@mantine/hooks": "^8.3.18",
"@mantine/modals": "^8.3.18",
"@mantine/notifications": "^8.3.18",
"@modelcontextprotocol/sdk": "^1.29.0",
"@prisma/client": "6", "@prisma/client": "6",
"@tanstack/react-query": "^5.95.2", "@tanstack/react-query": "^5.95.2",
"@tanstack/react-router": "^1.168.10", "@tanstack/react-router": "^1.168.10",
"@xyflow/react": "^12.6.4",
"dayjs": "^1.11.20",
"elkjs": "^0.9.3",
"elysia": "^1.4.28", "elysia": "^1.4.28",
"minio": "^8.0.7",
"postcss": "^8.5.8", "postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
@@ -26,11 +34,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",
@@ -105,6 +116,8 @@
"@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="], "@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="],
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="],
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
@@ -173,6 +186,8 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -187,16 +202,32 @@
"@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="], "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="],
"@mantine/charts": ["@mantine/charts@9.0.0", "", { "peerDependencies": { "@mantine/core": "9.0.0", "@mantine/hooks": "9.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", "recharts": ">=3.2.1" } }, "sha512-TnbjiT2tXZDAQWZrv/+Xu3JKYjPiTfO5jSIbcwnxZSVtLI+PIxA7zrSps+it/Nx3ch8GHpDizJ7UArC0UfmNkQ=="], "@mantine/charts": ["@mantine/charts@8.3.18", "", { "peerDependencies": { "@mantine/core": "8.3.18", "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x", "recharts": ">=2.13.3" } }, "sha512-oudif3EUH7Nb9DPm0abAPxpFYDWWjR3k2S5ll0/CcB+pJzlhwaBG19QwpOJaRA6VAvAXDDKOXCO4mi9XCEN78g=="],
"@mantine/core": ["@mantine/core@8.3.18", "", { "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.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA=="], "@mantine/core": ["@mantine/core@8.3.18", "", { "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.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA=="],
"@mantine/dates": ["@mantine/dates@8.3.18", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "@mantine/core": "8.3.18", "@mantine/hooks": "8.3.18", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-FHx5teJOhupI0gO2o5evtVYQEdqOjayOkLRhEQfB5Nc5DvcysfPfmNILGkc1Nrp9ZQeQWKLT9qr+CkcCXwHOaw=="],
"@mantine/hooks": ["@mantine/hooks@8.3.18", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw=="], "@mantine/hooks": ["@mantine/hooks@8.3.18", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw=="],
"@mantine/modals": ["@mantine/modals@8.3.18", "", { "peerDependencies": { "@mantine/core": "8.3.18", "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-JfPDS4549L314SxFPC1x6CbKwzh82OdnIzwgMxPCVNsWLKV2vEHHUH/fzUYj4Wli6IBrsW4cufjMj9BTj3hm3Q=="],
"@mantine/notifications": ["@mantine/notifications@8.3.18", "", { "dependencies": { "@mantine/store": "8.3.18", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.18", "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-IpQ0lmwbigTBbZCR6iSYWqIOKEx1tlcd7PcEJ5M5X1qeVSY/N3mmDQt1eJmObvcyDeL5cTJMbSA9UPqhRqo9jw=="],
"@mantine/store": ["@mantine/store@8.3.18", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-i+QRTLmZzLldea0egtUVnGALd6UMIu8jd44nrNWBSNIXJU/8B6rMlC6gyX+l4szopZSuOaaNJIXkqRdC1gQsVg=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
"@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="],
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
"@playwright/mcp": ["@playwright/mcp@0.0.70", "", { "dependencies": { "playwright": "1.60.0-alpha-1774999321000", "playwright-core": "1.60.0-alpha-1774999321000" }, "bin": { "playwright-mcp": "cli.js" } }, "sha512-Kl0a6l9VL8rvT1oBou3hS5yArjwWV9UlwAkq+0skfK1YVg8XfmmNaAmwZhMeNx/ZhGiWXfCllo6rD/jvZz+WuA=="],
"@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
"@prisma/client": ["@prisma/client@6.19.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg=="], "@prisma/client": ["@prisma/client@6.19.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg=="],
"@prisma/config": ["@prisma/config@6.19.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ=="], "@prisma/config": ["@prisma/config@6.19.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ=="],
@@ -247,6 +278,12 @@
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -291,6 +328,8 @@
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
@@ -299,12 +338,18 @@
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
@@ -315,12 +360,24 @@
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"@xyflow/react": ["@xyflow/react@12.10.2", "", { "dependencies": { "@xyflow/system": "0.0.76", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ=="],
"@xyflow/system": ["@xyflow/system@0.0.76", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -331,6 +388,8 @@
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
@@ -353,16 +412,28 @@
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001782", "", {}, "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw=="], "caniuse-lite": ["caniuse-lite@1.0.30001782", "", {}, "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw=="],
@@ -375,6 +446,8 @@
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
"classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@@ -387,12 +460,22 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -401,6 +484,10 @@
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
"d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
@@ -411,6 +498,8 @@
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
@@ -419,18 +508,28 @@
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="],
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
@@ -443,26 +542,44 @@
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.329", "", {}, "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ=="], "electron-to-chromium": ["electron-to-chromium@1.5.329", "", {}, "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ=="],
"elkjs": ["elkjs@0.9.3", "", {}, "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="],
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
@@ -471,12 +588,22 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.4.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
@@ -485,8 +612,16 @@
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], "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-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-xml-builder": ["fast-xml-builder@1.1.5", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA=="],
"fast-xml-parser": ["fast-xml-parser@5.7.1", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.5", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -495,7 +630,17 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
@@ -503,8 +648,12 @@
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
@@ -515,18 +664,36 @@
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"hono": ["hono@4.12.15", "", {}, "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -537,14 +704,24 @@
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isbot": ["isbot@5.1.37", "", {}, "sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ=="], "isbot": ["isbot@5.1.37", "", {}, "sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
@@ -571,16 +748,34 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"minio": ["minio@8.0.7", "", { "dependencies": { "async": "^3.2.4", "block-stream2": "^2.1.0", "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^5.3.4", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", "xml2js": "^0.5.0 || ^0.6.2" } }, "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
@@ -591,8 +786,14 @@
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
@@ -601,7 +802,15 @@
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
@@ -611,8 +820,14 @@
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
"playwright-core": ["playwright-core@1.60.0-alpha-1774999321000", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-ams3Zo4VXxeOg5ZTTh16GkE8g48Bmxo/9pg9gXl9SVKlVohCU7Jaog7XntY8yFuzENA6dJc1Fz7Z/NNTm9nGEw=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
@@ -633,6 +848,10 @@
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
@@ -643,6 +862,14 @@
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
"query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
@@ -665,6 +892,10 @@
"react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="], "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
@@ -677,20 +908,48 @@
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="], "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="], "seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="],
"seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="], "seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
@@ -701,12 +960,26 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="],
"stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="],
"streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
"strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="],
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
"sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="], "sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
@@ -723,6 +996,8 @@
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
"through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
@@ -731,6 +1006,8 @@
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -739,6 +1016,8 @@
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typed-query-selector": ["typed-query-selector@2.12.1", "", {}, "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA=="], "typed-query-selector": ["typed-query-selector@2.12.1", "", {}, "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA=="],
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
@@ -747,6 +1026,8 @@
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
@@ -765,6 +1046,8 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="], "vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
@@ -773,12 +1056,18 @@
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -789,8 +1078,14 @@
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -799,32 +1094,82 @@
"@kitajs/ts-html-plugin/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], "@kitajs/ts-html-plugin/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"@playwright/mcp/playwright": ["playwright@1.60.0-alpha-1774999321000", "", { "dependencies": { "playwright-core": "1.60.0-alpha-1774999321000" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-Bd5DkzYKG+2g1jLO6NeTXmGLbBYSFffJIOsR4l4hUBkJvzvGGdLZ7jZb2tOtb0WIoWXQKdQj3Ap6WthV4DBS8w=="],
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
"@tanstack/router-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"playwright/playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="], "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
"send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"@kitajs/ts-html-plugin/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "@kitajs/ts-html-plugin/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"@kitajs/ts-html-plugin/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "@kitajs/ts-html-plugin/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"@kitajs/ts-html-plugin/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "@kitajs/ts-html-plugin/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw=="],
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"@kitajs/ts-html-plugin/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "@kitajs/ts-html-plugin/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"@kitajs/ts-html-plugin/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "@kitajs/ts-html-plugin/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],

BIN
bun.lockb

Binary file not shown.

78
compose.yml Normal file
View File

@@ -0,0 +1,78 @@
services:
monitoring-app:
image: ghcr.io/bipprojectbali/monitoring-app:stg-latest
container_name: monitoring-app-stg
restart: unless-stopped
environment:
# App
- PORT=${PORT:-3000}
- NODE_ENV=${NODE_ENV:-production}
- BUN_PUBLIC_BASE_URL=${BUN_PUBLIC_BASE_URL}
# Database
- DATABASE_URL=${DATABASE_URL}
- DIRECT_URL=${DIRECT_URL}
# Google OAuth
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
# Super Admin
- SUPER_ADMIN_EMAIL=${SUPER_ADMIN_EMAIL}
# API Key
- API_KEY=${API_KEY}
# MinIO (object storage)
- MINIO_ENDPOINT=${MINIO_ENDPOINT}
- MINIO_PORT=${MINIO_PORT:-443}
- MINIO_USE_SSL=${MINIO_USE_SSL:-true}
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
- MINIO_BUCKET=${MINIO_BUCKET}
- MINIO_UPLOAD_DIR=${MINIO_UPLOAD_DIR:-bug-reports}
# Redis (optional — app logs feature)
- REDIS_URL=${REDIS_URL:-}
networks:
- public-net
- postgres-net-stg
depends_on:
migrate:
condition: service_completed_successfully
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
memory: 512M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
labels:
- "traefik.enable=true"
- "traefik.docker.network=public-net"
- "traefik.http.routers.monitoring-app.rule=Host(`monitoring-stg.wibudev.com`)"
- "traefik.http.routers.monitoring-app.entrypoints=websecure"
- "traefik.http.routers.monitoring-app.tls=true"
- "traefik.http.routers.monitoring-app.tls.certresolver=letsencrypt"
- "traefik.http.services.monitoring-app.loadbalancer.server.port=3000"
migrate:
image: ghcr.io/bipprojectbali/monitoring-app:stg-latest
container_name: monitoring-app-stg-migrate
restart: "no"
# `migrate deploy` only applies existing migrations from prisma/migrations/.
# Safer than `migrate dev --name auto` which auto-generates new migrations
# from schema diff (risk of drift in production).
# Seed runs only if SEED_ON_DEPLOY=true (idempotent — wipe-and-reseed by
# design; recommend leaving false for production with real customer data).
entrypoint: ["sh", "-c", "bunx prisma migrate deploy && if [ \"$$SEED_ON_DEPLOY\" = \"true\" ]; then bun prisma/seed.ts; fi"]
environment:
- DATABASE_URL=${DIRECT_URL}
- SEED_ON_DEPLOY=${SEED_ON_DEPLOY:-false}
networks:
- postgres-net-stg
networks:
public-net:
external: true
postgres-net-stg:
external: true

66
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,66 @@
# Architecture
## Server
Elysia.js on Bun. All API routes are in `src/app.ts` as `createApp()` — testable via `app.handle(request)` without starting a server. `src/index.tsx` adds Vite middleware (dev) or static serving (prod) and calls `.listen()`. `src/serve.ts` is the dev entry point (dynamic import workaround for Bun EADDRINUSE race).
## Database
PostgreSQL via Prisma v6. Client generated to `./generated/prisma` (gitignored — run `bun run db:generate` after checkout or schema changes).
**Schema models:** `User`, `Session`, `App`, `Log`, `Bug`, `BugImage`, `BugLog`
**Enums:** `Role` (ADMIN, DEVELOPER), `BugStatus` (OPEN, ON_HOLD, IN_PROGRESS, RESOLVED, RELEASED, CLOSED), `BugSource` (QC, SYSTEM, USER), `LogType` (CREATE, UPDATE, DELETE, LOGIN, LOGOUT)
Import the singleton: `import { prisma } from './lib/db'`
## Auth & Roles
Session-based auth with HttpOnly cookies stored in the DB (24h expiry). Two roles: `DEVELOPER` (super admin) and `ADMIN`. Users listed in `SUPER_ADMIN_EMAIL` env var are auto-promoted to DEVELOPER on login.
Endpoints: `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/session`
Auth state on the frontend is managed via `useSession()` / `useLogin()` / `useLogout()` in `src/frontend/hooks/useAuth.ts` (TanStack Query).
## Frontend
React 19 + Vite 8 (middleware mode in dev). TanStack Router with file-based routing in `src/frontend/routes/`. All routes are wrapped in `DashboardLayout` from `src/frontend/components/DashboardLayout.tsx`.
**Route structure:**
- `/` → redirect
- `/login` → login page
- `/dashboard` → stats overview
- `/apps` → app list
- `/apps/$appId` → per-app layout with nested routes: `index`, `errors`, `logs`, `users`, `villages`, `orders`, `products`, `payments`
- `/users` → operator management
- `/logs` → system activity log
- `/bug-reports` → cross-app bug reports
- `/profile` → user profile
**App configs** are defined in `src/frontend/config/appMenus.ts` — each app has an ID and a menu list. Currently active: `desa-plus`. Add new app entries here to register them.
**routeTree.gen.ts** is auto-generated by the TanStack Router Vite plugin — never edit it manually.
**UI:** Mantine v8, dark theme forced (`#242424`). Charts use `@mantine/charts` (recharts under the hood). Icons from `react-icons/tb`.
## API Structure
All API routes live in `src/app.ts`. Key groups:
- `/api/auth/*` — authentication
- `/api/dashboard/*` — stats and recent errors
- `/api/apps`, `/api/apps/:appId` — app listing and detail
- `/api/bugs`, `/api/bugs/:id/status`, `/api/bugs/:id/feedback` — bug report CRUD
- `/api/operators`, `/api/operators/:id` — user management
- `/api/logs` — system activity log
- `/api/system/status` — health check with DB connectivity
## Logging
`createSystemLog(userId, type, message)` from `src/lib/logger.ts` writes to the `Log` model. Call it for any significant user action (login/logout/CRUD). Logging errors are swallowed so they never break the main flow.
## Environment Variables
Required: `DATABASE_URL`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`
Optional: `PORT` (default 3000), `NODE_ENV`, `REACT_EDITOR`, `SUPER_ADMIN_EMAIL` (comma-separated)
Validated at startup in `src/lib/env.ts` — missing required vars throw immediately.

6
docs/DEV_TOOLS.md Normal file
View File

@@ -0,0 +1,6 @@
# Dev Tools
- **Click-to-source:** `Ctrl+Shift+Cmd+C` toggles inspector. Custom Vite plugin in `src/vite.ts` injects `data-inspector-*` attributes; reads original source from disk for accurate line numbers.
- **HMR:** Vite 8 + `@vitejs/plugin-react` v6. `dedupeRefreshPlugin` in `src/vite.ts` prevents double React Refresh injection.
- **Editor:** Set `REACT_EDITOR` env var. `zed`/`subl` use `file:line:col`; others get `--goto file:line:col`.
- **Playwright MCP:** `bun run mcp:playwright` starts headless browser MCP server (config in `.qwen/settings.json`).

6
docs/TESTING.md Normal file
View File

@@ -0,0 +1,6 @@
# Testing
- **Unit:** env, DB connection, bcrypt — in `tests/unit/`
- **Integration:** `createApp().handle(new Request(...))` — no running server needed, use these for mutations
- **E2E:** Lightpanda browser via CDP (`ws://127.0.0.1:9222`). App URLs use `host.docker.internal` from inside Docker. Lightpanda executes JS but POST fetch returns 407 — use integration tests for anything that writes data.
- **Helpers:** `tests/helpers.ts``createTestApp()`, `seedTestUser()`, `createTestSession()`, `cleanupTestData()`

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

@@ -1,9 +1,10 @@
{ {
"name": "bun-react-template", "name": "bun-react-template",
"version": "0.1.0", "version": "0.1.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"claude": "set -a && source .env && set +a && claude",
"dev": "bun --watch src/serve.ts", "dev": "bun --watch src/serve.ts",
"build": "vite build", "build": "vite build",
"start": "NODE_ENV=production bun src/index.tsx", "start": "NODE_ENV=production bun src/index.tsx",
@@ -18,20 +19,29 @@
"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",
"@elysiajs/eden": "^1.4.9", "@elysiajs/eden": "^1.4.9",
"@elysiajs/html": "^1.4.0", "@elysiajs/html": "^1.4.0",
"@mantine/charts": "^9.0.0", "@elysiajs/swagger": "^1.3.1",
"@mantine/charts": "^8.3.0",
"@mantine/core": "^8.3.18", "@mantine/core": "^8.3.18",
"@mantine/dates": "^8.3.0",
"@mantine/hooks": "^8.3.18", "@mantine/hooks": "^8.3.18",
"@mantine/modals": "^8.3.18",
"@mantine/notifications": "^8.3.18", "@mantine/notifications": "^8.3.18",
"@modelcontextprotocol/sdk": "^1.29.0",
"@prisma/client": "6", "@prisma/client": "6",
"@tanstack/react-query": "^5.95.2", "@tanstack/react-query": "^5.95.2",
"@tanstack/react-router": "^1.168.10", "@tanstack/react-router": "^1.168.10",
"@xyflow/react": "^12.6.4",
"dayjs": "^1.11.20",
"elkjs": "^0.9.3",
"elysia": "^1.4.28", "elysia": "^1.4.28",
"minio": "^8.0.7",
"postcss": "^8.5.8", "postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
@@ -43,11 +53,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,12 @@
/*
Warnings:
- Changed the type of `type` on the `log` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- CreateEnum
CREATE TYPE "LogType" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'LOGIN', 'LOGOUT');
-- AlterTable
ALTER TABLE "log" DROP COLUMN "type",
ADD COLUMN "type" "LogType" NOT NULL;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "user" ADD COLUMN "image" TEXT;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "bug_log" ALTER COLUMN "userId" DROP NOT NULL;

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- The `app` column on the `bug` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "bug" DROP COLUMN "app",
ADD COLUMN "app" TEXT;
-- DropEnum
DROP TYPE "App";

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

@@ -0,0 +1,21 @@
-- AlterEnum: add USER back to Role
BEGIN;
CREATE TYPE "Role_new" AS ENUM ('USER', '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 'USER';
COMMIT;
-- AlterTable: make password nullable, change default role
ALTER TABLE "user"
ALTER COLUMN "password" DROP NOT NULL,
ALTER COLUMN "role" SET DEFAULT 'USER';
-- AlterTable: add googleId column
ALTER TABLE "user" ADD COLUMN "googleId" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "user_googleId_key" ON "user"("googleId");

View File

@@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE "app_config" (
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "app_config_pkey" PRIMARY KEY ("key")
);

View File

@@ -0,0 +1,15 @@
-- AlterTable: tambah urlApi dan apiKey ke App
ALTER TABLE "App" ADD COLUMN "urlApi" TEXT;
ALTER TABLE "App" ADD COLUMN "apiKey" TEXT;
-- DataMigration: pindahkan nilai dari app_config ke App sebelum drop
UPDATE "App"
SET "urlApi" = (SELECT value FROM app_config WHERE key = 'URL_API_DESA_PLUS')
WHERE id = 'desa-plus';
UPDATE "App"
SET "apiKey" = (SELECT value FROM app_config WHERE key = 'API_KEY_DESA_PLUS')
WHERE id = 'desa-plus';
-- DropTable
DROP TABLE "app_config";

View File

@@ -0,0 +1 @@
ALTER TABLE "App" ADD COLUMN "active" BOOLEAN NOT NULL DEFAULT true;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "App" ADD COLUMN "clientApiKey" TEXT;
CREATE UNIQUE INDEX "App_clientApiKey_key" ON "App"("clientApiKey");

View File

@@ -11,14 +11,9 @@ datasource db {
enum Role { enum Role {
USER USER
ADMIN ADMIN
SUPER_ADMIN
DEVELOPER DEVELOPER
} }
enum App{
desa_plus
hipmi
}
enum BugSource{ enum BugSource{
QC QC
@@ -35,13 +30,23 @@ enum BugStatus{
CLOSED CLOSED
} }
enum LogType{
CREATE
UPDATE
DELETE
LOGIN
LOGOUT
}
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
name String name String
email String @unique email String @unique
password String password String?
googleId String? @unique
role Role @default(USER) role Role @default(USER)
active Boolean @default(true) active Boolean @default(true)
image String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -66,10 +71,26 @@ model Session {
@@map("session") @@map("session")
} }
model App {
id String @id @default(uuid())
name String
version String?
minVersion String?
maintenance Boolean @default(false)
active Boolean @default(true)
urlApi String?
apiKey String?
clientApiKey String? @unique
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
type String type LogType
message String message String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -81,12 +102,12 @@ model Log {
model Bug { model Bug {
id String @id @default(uuid()) id String @id @default(uuid())
userId String? userId String?
app App appId String?
affectedVersion String affectedVersion String
device String device String
os String os String
status BugStatus status BugStatus
source BugSource source BugSource
description String description String
stackTrace String? stackTrace String?
fixedVersion String? fixedVersion String?
@@ -95,6 +116,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[]
@@ -116,17 +138,17 @@ model BugImage {
model BugLog { model BugLog {
id String @id @default(uuid()) id String @id @default(uuid())
bugId String bugId String
userId String userId String?
status BugStatus status BugStatus
description String description String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
bug Bug @relation(fields: [bugId], references: [id], onDelete: Cascade) bug Bug @relation(fields: [bugId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("bug_log") @@map("bug_log")
} }

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}`)
} }
} }

231
scripts/mcp-deploy.ts Normal file
View File

@@ -0,0 +1,231 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
const GH_TOKEN = process.env.GH_TOKEN ?? ''
const STACK_NAME = process.env.STACK_NAME ?? ''
const BASE_URL = process.env.BASE_URL ?? '' // https://api.github.com/repos/owner/repo
const STG_URL = process.env.STG_URL ?? '' // https://monitoring-stg.example.com
const VERSION_PATH = process.env.VERSION_PATH ?? '/api/system/version'
// ─── GitHub API helpers ────────────────────────────────────────────────────────
const ghHeaders = {
Authorization: `Bearer ${GH_TOKEN}`,
Accept: 'application/vnd.github+json',
'Content-Type': 'application/json',
'X-GitHub-Api-Version': '2022-11-28',
}
async function triggerWorkflow(workflow: string, inputs: Record<string, string>) {
const res = await fetch(`${BASE_URL}/actions/workflows/${workflow}/dispatches`, {
method: 'POST',
headers: ghHeaders,
body: JSON.stringify({ ref: 'main', inputs }),
})
if (!res.ok) throw new Error(`GitHub API error ${res.status}: ${await res.text()}`)
}
async function waitForWorkflow(
workflow: string,
afterTime: Date,
timeoutMs = 600_000,
): Promise<{ conclusion: string; url: string }> {
const deadline = Date.now() + timeoutMs
await Bun.sleep(8_000) // tunggu run muncul di API
while (Date.now() < deadline) {
const res = await fetch(`${BASE_URL}/actions/workflows/${workflow}/runs?per_page=5`, {
headers: ghHeaders,
})
const data = await res.json() as { workflow_runs: any[] }
const run = data.workflow_runs?.find(
(r: any) => new Date(r.created_at) >= afterTime,
)
if (run) {
if (run.status === 'completed') {
return { conclusion: run.conclusion ?? 'failure', url: run.html_url }
}
}
await Bun.sleep(12_000)
}
throw new Error(`Workflow ${workflow} timeout setelah ${timeoutMs / 1000}s`)
}
// ─── Shell helpers ─────────────────────────────────────────────────────────────
async function sh(cmd: string[]): Promise<{ out: string; err: string; ok: boolean }> {
const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe', cwd: process.cwd() })
const [out, err, code] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
])
return { out: out.trim(), err: err.trim(), ok: code === 0 }
}
// ─── MCP Server ────────────────────────────────────────────────────────────────
const server = new McpServer({ name: 'deploy-stg', version: '1.0.0' })
// ─── Tool: publish (manual, single step) ──────────────────────────────────────
server.tool(
'publish',
'Trigger publish.yml untuk build & push Docker image staging',
{ tag: z.string().describe('Image tag, contoh: 1.0.0') },
async ({ tag }) => {
await triggerWorkflow('publish.yml', { stack_env: 'stg', tag })
return { content: [{ type: 'text', text: `✅ publish.yml dipicu → stg-${tag}` }] }
},
)
// ─── Tool: repull (manual, single step) ───────────────────────────────────────
server.tool(
'repull',
'Trigger re-pull.yml untuk redeploy stack staging di Portainer',
{},
async () => {
await triggerWorkflow('re-pull.yml', { stack_name: STACK_NAME, stack_env: 'stg' })
return { content: [{ type: 'text', text: `✅ re-pull.yml dipicu → ${STACK_NAME}-stg` }] }
},
)
// ─── Tool: deploy (full pipeline) ─────────────────────────────────────────────
server.tool(
'deploy',
[
'Full deploy pipeline ke staging:',
'1. Cek pending migrations',
'2. Version bump di package.json',
'3. Commit & push ke build/stg',
'4. Trigger publish.yml → tunggu selesai',
'5. Trigger re-pull.yml → tunggu selesai',
'6. Cek version di staging & local untuk konfirmasi',
].join('\n'),
{ tag: z.string().describe('Versi baru, contoh: 1.2.3') },
async ({ tag }) => {
const log: string[] = []
// ── 1. Cek & jalankan migrasi jika ada ─────────────────────────────────
const migrateStatus = await sh(['bunx', 'prisma', 'migrate', 'status'])
if (!migrateStatus.ok || migrateStatus.out.includes('not yet been applied')) {
log.push('⏳ Ada pending migrations — menjalankan migrate deploy...')
const migrateRun = await sh(['bunx', 'prisma', 'migrate', 'deploy'])
if (!migrateRun.ok) {
return {
content: [{
type: 'text',
text: [
...log,
'❌ Migrate deploy gagal:',
migrateRun.err || migrateRun.out,
].join('\n'),
}],
}
}
log.push('✅ Migrations: deployed')
} else {
log.push('✅ Migrations: up to date')
}
// ── 2. Version bump ──────────────────────────────────────────────────────
const pkgPath = `${process.cwd()}/package.json`
const pkg = await Bun.file(pkgPath).json()
const prevVersion = pkg.version as string
pkg.version = tag
await Bun.write(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
log.push(`✅ Version bump: ${prevVersion}${tag}`)
// ── 3. Commit & push build/stg ───────────────────────────────────────────
await sh(['git', 'add', 'package.json'])
const commit = await sh(['git', 'commit', '-m', `chore: bump version to ${tag}`])
if (!commit.ok) {
return { content: [{ type: 'text', text: `❌ git commit gagal:\n${commit.err}` }] }
}
log.push('✅ Committed')
const push = await sh(['git', 'push', 'origin', 'HEAD:build/stg'])
if (!push.ok) {
return { content: [{ type: 'text', text: `❌ git push gagal:\n${push.err}` }] }
}
log.push('✅ Pushed → build/stg')
// ── 4. Publish workflow ──────────────────────────────────────────────────
log.push('⏳ Menjalankan publish.yml...')
const publishTriggeredAt = new Date()
await triggerWorkflow('publish.yml', { stack_env: 'stg', tag })
const publish = await waitForWorkflow('publish.yml', publishTriggeredAt)
if (publish.conclusion !== 'success') {
return {
content: [{
type: 'text',
text: [
...log,
`❌ publish.yml ${publish.conclusion}`,
`Detail: ${publish.url}`,
].join('\n'),
}],
}
}
log.push(`✅ publish.yml sukses → ${publish.url}`)
// ── 5. Re-pull workflow ──────────────────────────────────────────────────
log.push('⏳ Menjalankan re-pull.yml...')
const repullTriggeredAt = new Date()
await triggerWorkflow('re-pull.yml', { stack_name: STACK_NAME, stack_env: 'stg' })
const repull = await waitForWorkflow('re-pull.yml', repullTriggeredAt)
if (repull.conclusion !== 'success') {
return {
content: [{
type: 'text',
text: [
...log,
`❌ re-pull.yml ${repull.conclusion}`,
`Detail: ${repull.url}`,
].join('\n'),
}],
}
}
log.push(`✅ re-pull.yml sukses → ${repull.url}`)
// ── 6. Cek version ───────────────────────────────────────────────────────
await Bun.sleep(5_000) // tunggu container restart
log.push('⏳ Mengecek version di staging...')
const localCommitProc = await sh(['git', 'rev-parse', '--short', 'HEAD'])
const localCommit = localCommitProc.out
let stgInfo: { version?: string; commit?: string } = {}
try {
const versionRes = await fetch(`${STG_URL}${VERSION_PATH}`)
stgInfo = await versionRes.json()
} catch (e) {
log.push(`⚠️ Gagal mengecek version staging: ${e}`)
}
const versionMatch = stgInfo.version === tag
const commitMatch = stgInfo.commit === localCommit
log.push('')
log.push('─── Version Check ───────────────────────────')
log.push(`Local : version=${tag}, commit=${localCommit}`)
log.push(`Staging: version=${stgInfo.version ?? '?'}, commit=${stgInfo.commit ?? '?'}`)
log.push(versionMatch && commitMatch
? '✅ Staging sudah terupdate dan sesuai local'
: `⚠️ Mismatch — version: ${versionMatch ? 'OK' : 'BEDA'}, commit: ${commitMatch ? 'OK' : 'BEDA'}`,
)
return { content: [{ type: 'text', text: log.join('\n') }] }
},
)
const transport = new StdioServerTransport()
await server.connect(transport)

1682
src/app.ts

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
import { ColorSchemeScript, MantineProvider, createTheme } from '@mantine/core' import { ColorSchemeScript, MantineProvider, createTheme } from '@mantine/core'
import '@mantine/core/styles.css' import '@mantine/core/styles.css'
import '@mantine/dates/styles.css'
import '@mantine/notifications/styles.css' import '@mantine/notifications/styles.css'
import { ModalsProvider } from '@mantine/modals'
import { Notifications } from '@mantine/notifications' import { Notifications } from '@mantine/notifications'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRouter, RouterProvider } from '@tanstack/react-router' import { createRouter, RouterProvider } from '@tanstack/react-router'
@@ -64,9 +66,11 @@ export function App() {
<ColorSchemeScript defaultColorScheme="auto" /> <ColorSchemeScript defaultColorScheme="auto" />
<MantineProvider theme={theme} defaultColorScheme="auto"> <MantineProvider theme={theme} defaultColorScheme="auto">
<Notifications /> <Notifications />
<QueryClientProvider client={queryClient}> <ModalsProvider>
<RouterProvider router={router} /> <QueryClientProvider client={queryClient}>
</QueryClientProvider> <RouterProvider router={router} />
</QueryClientProvider>
</ModalsProvider>
</MantineProvider> </MantineProvider>
</> </>
) )

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,15 +1,15 @@
import { import { BarChart, LineChart } from '@mantine/charts'
Paper, import {
Stack,
Text,
Group,
ThemeIcon,
Box,
Badge, Badge,
Box,
Group,
Paper,
Stack,
Text,
ThemeIcon,
useMantineTheme useMantineTheme
} from '@mantine/core' } from '@mantine/core'
import { LineChart, BarChart } from '@mantine/charts' import { TbArrowUpRight, TbChartBar, TbTimeline } from 'react-icons/tb'
import { TbTimeline, TbChartBar, TbArrowUpRight } from 'react-icons/tb'
interface ChartProps { interface ChartProps {
data?: any[] data?: any[]
@@ -18,7 +18,7 @@ interface ChartProps {
export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) { export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) {
const theme = useMantineTheme() const theme = useMantineTheme()
return ( return (
<Paper withBorder p="xl" radius="2xl" className="glass h-full"> <Paper withBorder p="xl" radius="2xl" className="glass h-full">
<Stack gap="md" h="100%"> <Stack gap="md" h="100%">
@@ -32,9 +32,14 @@ export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) {
<Text size="xs" c="dimmed">Trend over the last 7 days</Text> <Text size="xs" c="dimmed">Trend over the last 7 days</Text>
</Box> </Box>
</Group> </Group>
<Badge variant="light" color="blue" size="sm" rightSection={<TbArrowUpRight size={12} />}> {
{isLoading ? '...' : 'Live'} isLoading && (
</Badge> <Badge variant="light" color="blue" size="sm" rightSection={<TbArrowUpRight size={12} />}>
...
</Badge>
)
}
</Group> </Group>
<Box h={300} mt="lg"> <Box h={300} mt="lg">
@@ -48,6 +53,9 @@ export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) {
gridAxis="x" gridAxis="x"
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
tooltipProps={{
allowEscapeViewBox: { x: true, y: false },
}}
styles={{ styles={{
root: { root: {
'.recharts-line-curve': { '.recharts-line-curve': {
@@ -86,17 +94,38 @@ export function VillageComparisonBarChart({ data = [], isLoading }: ChartProps)
h={300} h={300}
data={data} data={data}
dataKey="village" dataKey="village"
series={[{ name: 'activity', color: 'indigo.6' }]} series={[{ name: 'activity', color: 'blue.6' }]} // Menggunakan warna dari theme
withTooltip withTooltip
tickLine="none"
gridAxis="y"
barProps={{ barProps={{
radius: [8, 8, 4, 4], radius: [8, 8, 0, 0],
fill: 'url(#barGradient)', // Menggunakan gradient yang Anda buat
}} }}
styles={{ tooltipProps={{
bar: { cursor: { fill: '#373A40', opacity: 0.4 },
fill: 'url(#barGradient)', allowEscapeViewBox: { x: false, y: false },
content: ({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div style={{
backgroundColor: '#1A1B1E',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #373A40',
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
pointerEvents: 'none', // Sangat penting agar tidak mengganggu hover
whiteSpace: 'nowrap' // Mencegah teks turun ke bawah
}}>
<div style={{ fontSize: '12px', fontWeight: 600, color: '#fff', marginBottom: '4px' }}>
{payload[0].payload.village}
</div>
<div style={{ fontSize: '11px', color: '#2563EB' }}>
Activity: <span style={{ fontWeight: 700 }}>{payload[0].value}</span>
</div>
</div>
);
} }
return null;
},
}} }}
> >
<defs> <defs>

View File

@@ -1,35 +1,46 @@
import { APP_CONFIGS } from '@/frontend/config/appMenus' import { APP_CONFIGS } from '@/frontend/config/appMenus'
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
import React from 'react'
import { import {
ActionIcon, ActionIcon,
Alert,
AppShell, AppShell,
Avatar, Avatar,
Box, Box,
Burger, Burger,
Button, Button,
Center,
Group, Group,
Loader,
LoadingOverlay,
Menu, Menu,
NavLink, NavLink,
Select, Select,
Stack, Stack,
Text, Text,
ThemeIcon ThemeIcon,
Title,
useComputedColorScheme,
useMantineColorScheme
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core' 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,
TbApps, TbApps,
TbArrowLeft, TbArrowLeft,
TbChevronRight, TbChevronRight,
TbClock,
TbDashboard, TbDashboard,
TbDeviceMobile, TbDeviceMobile,
TbHistory,
TbLogout, TbLogout,
TbSettings,
TbUserCircle,
TbSun,
TbMoon, TbMoon,
TbSettings,
TbSun,
TbUser, TbUser,
TbHistory TbUserCircle
} from 'react-icons/tb' } from 'react-icons/tb'
interface DashboardLayoutProps { interface DashboardLayoutProps {
@@ -48,16 +59,68 @@ 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, isLoading: sessionLoading } = useSession()
const user = sessionData?.user
const logout = useLogout()
// Redirect USER role to profile (pending approval)
React.useEffect(() => {
if (!sessionLoading && user?.role === 'USER') {
navigate({ to: '/profile' })
}
}, [user?.role, sessionLoading, navigate])
// ─── 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' },
{ label: 'Log Activity', icon: TbHistory, to: '/logs' }, { label: 'Log Activity', icon: TbHistory, to: '/logs' },
{ label: 'Error Reports', icon: TbAlertTriangle, to: '/bug-reports' },
{ label: 'Users', icon: TbUser, to: '/users' }, { label: 'Users', icon: TbUser, to: '/users' },
] ]
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()
}
// Prevent dashboard flash for USER role while redirect is happening
if (sessionLoading || user?.role === 'USER') {
return (
<Center mih="100vh">
<LoadingOverlay visible />
</Center>
)
}
return ( return (
<AppShell <AppShell
header={{ height: 70 }} header={{ height: 70 }}
@@ -112,21 +175,41 @@ 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.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>
@@ -157,10 +240,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"
@@ -198,16 +279,6 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
variant="filled" variant="filled"
color="brand-blue" color="brand-blue"
className="sidebar-nav-item" className="sidebar-nav-item"
styles={(theme) => ({
root: {
borderRadius: theme.radius.md,
transition: 'all 0.2s ease',
'&[data-active]': {
background: 'var(--gradient-blue-purple)',
fontWeight: 600,
},
},
})}
/> />
) )
})} })}
@@ -222,19 +293,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

@@ -1,76 +1,51 @@
import { import {
Table, Badge,
Badge,
Text,
Paper,
Group,
Drawer,
Stack,
Divider,
Code,
Button,
Box, Box,
Button,
Code,
Divider,
Drawer,
Group,
Paper,
ScrollArea, ScrollArea,
Stack,
Table,
Text,
Title Title
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
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 { useState } from 'react'
import { TbBug, TbExternalLink, TbHistory, TbMessageReport } 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 [showStackTrace, setShowStackTrace] = useState(false)
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)
setShowStackTrace(false)
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 +61,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 +72,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.Tr>
key={error.id} <Table.Td colSpan={5} align="center" py="xl">
onClick={() => handleRowClick(error)} Loading errors...
style={{ cursor: 'pointer' }} </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
key={error.id}
onClick={() => handleRowClick(error)}
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('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}</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,33 +143,40 @@ 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}>SOURCE</Text>
<Text fw={600}>{selectedError.village}</Text> <Text fw={600}>{selectedError.source}</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>
<Divider opacity={0.1} /> <Divider opacity={0.1} />
<Box> <Box>
<Text size="xs" fw={700} c="dimmed" mb="sm">STACK TRACE</Text> <Group justify="space-between" mb="sm">
<Code block color="red" style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6, border: '1px solid var(--mantine-color-default-border)' }}> <Text size="xs" fw={700} c="dimmed">STACK TRACE</Text>
{selectedError.stackTrace} <Button
</Code> variant="subtle"
size="compact-xs"
color="gray"
onClick={() => setShowStackTrace((v) => !v)}
>
{showStackTrace ? 'Hide' : 'Show'}
</Button>
</Group>
{showStackTrace && (
<Code block color="red" style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6, border: '1px solid var(--mantine-color-default-border)' }}>
{selectedError.stackTrace}
</Code>
)}
</Box> </Box>
<Group justify="flex-end" mt="xl">
<Button variant="light" color="gray" onClick={close}>Dismiss</Button>
<Button variant="gradient" gradient={{ from: 'red', to: 'orange' }}>Assign Technician</Button>
</Group>
</Stack> </Stack>
)} )}
</Drawer> </Drawer>

View File

@@ -1,26 +1,48 @@
export const API_BASE_URL = import.meta.env.VITE_URL_API_DESA_PLUS const DESA_PLUS_PROXY = '/api/proxy/desa-plus'
export const API_URLS = { export const API_URLS = {
getVillages: (page: number, search: string) => getVillages: (page: number, search: string) =>
`${API_BASE_URL}/api/monitoring/get-villages?page=${page}&search=${encodeURIComponent(search)}`, `${DESA_PLUS_PROXY}/api/monitoring/get-villages?page=${page}&search=${encodeURIComponent(search)}`,
infoVillages: (id: string) => infoVillages: (id: string) =>
`${API_BASE_URL}/api/monitoring/info-villages?id=${id}`, `${DESA_PLUS_PROXY}/api/monitoring/info-villages?id=${id}`,
gridVillages: (id: string) => gridVillages: (id: string) =>
`${API_BASE_URL}/api/monitoring/grid-villages?id=${id}`, `${DESA_PLUS_PROXY}/api/monitoring/grid-villages?id=${id}`,
graphLogVillages: (id: string, time: string) => graphLogVillages: (id: string, time: string) =>
`${API_BASE_URL}/api/monitoring/graph-log-villages?id=${id}&time=${time}`, `${DESA_PLUS_PROXY}/api/monitoring/graph-log-villages?id=${id}&time=${time}`,
getUsers: (page: number, search: string) => getUsers: (page: number, search: string) =>
`${API_BASE_URL}/api/monitoring/user?page=${page}&search=${encodeURIComponent(search)}`, `${DESA_PLUS_PROXY}/api/monitoring/user?page=${page}&search=${encodeURIComponent(search)}`,
getLogsAllVillages: (page: number, search: string) => getLogsAllVillages: (page: number, search: string) =>
`${API_BASE_URL}/api/monitoring/log-all-villages?page=${page}&search=${encodeURIComponent(search)}`, `${DESA_PLUS_PROXY}/api/monitoring/log-all-villages?page=${page}&search=${encodeURIComponent(search)}`,
getGridOverview: () => `${API_BASE_URL}/api/monitoring/grid-overview`, getGridOverview: () => `${DESA_PLUS_PROXY}/api/monitoring/grid-overview`,
getDailyActivity: () => `${API_BASE_URL}/api/monitoring/daily-activity`, getDailyActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/daily-activity`,
getComparisonActivity: () => `${API_BASE_URL}/api/monitoring/comparison-activity`, getComparisonActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/comparison-activity`,
postVersionUpdate: () => `${API_BASE_URL}/api/monitoring/version-update`, postVersionUpdate: () => `${DESA_PLUS_PROXY}/api/monitoring/version-update`,
createVillages: () => `${API_BASE_URL}/api/monitoring/create-villages`, createVillages: () => `${DESA_PLUS_PROXY}/api/monitoring/create-villages`,
createUser: () => `${API_BASE_URL}/api/monitoring/create-user`, createUser: () => `${DESA_PLUS_PROXY}/api/monitoring/create-user`,
listRole: () => `${API_BASE_URL}/api/monitoring/list-userrole-villages`, listRole: () => `${DESA_PLUS_PROXY}/api/monitoring/list-userrole-villages`,
listGroup: (id: string) => `${API_BASE_URL}/api/monitoring/list-group-villages?id=${id}`, listGroup: (id: string) => `${DESA_PLUS_PROXY}/api/monitoring/list-group-villages?id=${id}`,
listPosition: (id: string) => `${API_BASE_URL}/api/monitoring/list-position-villages?id=${id}`, listPosition: (id: string) => `${DESA_PLUS_PROXY}/api/monitoring/list-position-villages?id=${id}`,
editUser: () => `${API_BASE_URL}/api/monitoring/edit-user`, editUser: () => `${DESA_PLUS_PROXY}/api/monitoring/edit-user`,
updateStatusVillages: () => `${DESA_PLUS_PROXY}/api/monitoring/update-status-villages`,
editVillages: () => `${DESA_PLUS_PROXY}/api/monitoring/edit-villages`,
getGlobalLogs: (page: number, search: string, type: string, userId: string, dateFrom?: string, dateTo?: string) => {
const params = new URLSearchParams({ page: String(page), search, type, userId })
if (dateFrom) params.set('dateFrom', dateFrom)
if (dateTo) params.set('dateTo', dateTo)
return `/api/logs?${params}`
},
getLogOperators: () => `/api/logs/operators`,
getOperators: (page: number, search: string) =>
`/api/operators?page=${page}&search=${encodeURIComponent(search)}`,
getOperatorStats: () => `/api/operators/stats`,
createOperator: () => `/api/operators`,
editOperator: (id: string) => `/api/operators/${id}`,
deleteOperator: (id: string) => `/api/operators/${id}`,
getBugs: (page: number, search: string, app: string, status: string) =>
`/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`,
createBug: () => `/api/bugs`,
uploadImage: () => `/api/upload/image`,
updateBugStatus: (id: string) => `/api/bugs/${id}/status`,
updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`,
createLog: () => `/api/logs`,
} }

View File

@@ -1,13 +1,20 @@
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 = 'USER' | 'ADMIN' | 'DEVELOPER'
export function getDefaultRoute(role: Role): string {
if (role === 'DEVELOPER') return '/dev'
if (role === 'ADMIN') return '/dashboard'
return '/profile'
}
export interface User { export interface User {
id: string id: string
name: string name: string
email: string email: string
role: Role role: Role
image?: string | null
} }
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> { async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
@@ -41,12 +48,7 @@ export function useLogin() {
}), }),
onSuccess: (data) => { onSuccess: (data) => {
queryClient.setQueryData(['auth', 'session'], data) queryClient.setQueryData(['auth', 'session'], data)
// Super admin → dashboard, others → profile navigate({ to: getDefaultRoute(data.user.role) })
if (data.user.role === 'SUPER_ADMIN') {
navigate({ to: '/dashboard' })
} else {
navigate({ to: '/profile' })
}
}, },
}) })
} }

View File

@@ -0,0 +1,42 @@
import { useEffect, useRef, useState } from 'react'
import { useSession } from './useAuth'
export function usePresence() {
const { data } = useSession()
const [onlineUserIds, setOnlineUserIds] = useState<string[]>([])
const wsRef = useRef<WebSocket | null>(null)
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
useEffect(() => {
if (!data?.user) return
function connect() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
const ws = new WebSocket(`${proto}://${location.host}/ws/presence`)
wsRef.current = ws
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.type === 'presence') setOnlineUserIds(msg.online)
}
ws.onclose = () => {
wsRef.current = null
reconnectTimer.current = setTimeout(connect, 3000)
}
ws.onerror = () => ws.close()
}
connect()
return () => {
clearTimeout(reconnectTimer.current)
if (wsRef.current) {
wsRef.current.onclose = null
wsRef.current.close()
wsRef.current = null
}
}
}, [data?.user?.id, data?.user])
return { onlineUserIds }
}

View File

@@ -1,78 +1,261 @@
import { import {
Badge,
Container,
Group,
Stack,
Text,
Title,
Paper,
Accordion, Accordion,
ThemeIcon, Avatar,
TextInput, Badge,
Select,
Code,
Box, Box,
Button, Button,
Code,
Collapse,
FileInput,
Group,
Image,
Loader,
Modal,
Pagination,
Paper,
Select,
SimpleGrid, SimpleGrid,
Stack,
Text,
Textarea,
TextInput,
ThemeIcon,
Timeline,
Title
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import { useQuery } from '@tanstack/react-query'
import { createFileRoute, useParams } from '@tanstack/react-router' import { createFileRoute, useParams } from '@tanstack/react-router'
import { import { useState } from 'react'
TbAlertTriangle, import {
TbBug, TbAlertTriangle,
TbDeviceDesktop, TbBug,
TbDeviceMobile,
TbSearch,
TbFilter,
TbCircleCheck, TbCircleCheck,
TbUserCheck TbCircleX,
TbDeviceDesktop,
TbDeviceMobile,
TbFilter,
TbHistory,
TbPhoto,
TbPlus,
TbSearch
} from 'react-icons/tb' } from 'react-icons/tb'
import { API_URLS } from '../config/api'
export const Route = createFileRoute('/apps/$appId/errors')({ export const Route = createFileRoute('/apps/$appId/errors')({
component: AppErrorsPage, component: AppErrorsPage,
}) })
const mockErrors = [
{
id: 1,
title: 'NullPointerException: village_id is null',
message: 'Occurred during background sync with central server.',
version: '2.4.1',
device: 'PC Admin (Windows 10)',
time: '2 mins ago',
severity: 'critical',
users: 24,
frequency: 145,
stackTrace: 'at com.desa.sync.VillageManager.sync(VillageManager.java:45)\nat com.desa.sync.SyncService.onHandleIntent(SyncService.java:120)\nat android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:78)'
},
{
id: 2,
title: 'SocketTimeoutException: Connection reset by peer',
message: 'Failed to upload document: surat_kematian_01.pdf',
version: '2.4.0',
device: 'Android Tablet (Samsung Tab A8)',
time: '15 mins ago',
severity: 'high',
users: 5,
frequency: 12,
stackTrace: 'java.net.SocketTimeoutException: timeout\nat okio.Okio$4.newTimeoutException(Okio.java:232)\nat okio.AsyncTimeout.exit(AsyncTimeout.java:285)'
},
{
id: 3,
title: 'SQLiteException: no such column: village_id',
message: 'Failed to query local village profile database.',
version: '2.4.1',
device: 'PC Admin (Windows 7)',
time: '1 hour ago',
severity: 'medium',
users: 2,
frequency: 4,
stackTrace: 'java.io.IOException: No space left on device\nat java.io.FileOutputStream.writeBytes(Native Method)'
},
]
function AppErrorsPage() { function AppErrorsPage() {
const { appId } = useParams({ from: '/apps/$appId/errors' }) const { appId } = useParams({ from: '/apps/$appId/errors' })
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const [app, setApp] = useState(appId)
const [status, setStatus] = useState('all')
const [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
const [showStackTrace, setShowStackTrace] = useState<Record<string, boolean>>({})
const toggleLogs = (bugId: string) => {
setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
}
const toggleStackTrace = (bugId: string) => {
setShowStackTrace((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
}
const { data, isLoading, refetch } = useQuery({
queryKey: ['bugs', { page, search, app, status }],
queryFn: () =>
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()),
})
// Image Preview
const [previewImage, setPreviewImage] = useState<string | null>(null)
// Create Bug Modal Logic
const [opened, { open, close }] = useDisclosure(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [imageFiles, setImageFiles] = useState<File[]>([])
const [createForm, setCreateForm] = useState({
description: '',
app: appId,
source: 'USER',
affectedVersion: '',
device: '',
os: '',
stackTrace: '',
})
// Update Status Modal Logic
const [updateModalOpened, { open: openUpdateModal, close: closeUpdateModal }] = useDisclosure(false)
const [isUpdating, setIsUpdating] = useState(false)
const [selectedBugId, setSelectedBugId] = useState<string | null>(null)
const [updateForm, setUpdateForm] = useState({
status: '',
description: '',
})
// Feedback Modal Logic
const [feedbackModalOpened, { open: openFeedbackModal, close: closeFeedbackModal }] = useDisclosure(false)
const [isUpdatingFeedback, setIsUpdatingFeedback] = useState(false)
const [feedbackForm, setFeedbackForm] = useState({
feedBack: '',
})
const handleUpdateFeedback = async () => {
if (!selectedBugId || !feedbackForm.feedBack) return
setIsUpdatingFeedback(true)
try {
const res = await fetch(API_URLS.updateBugFeedback(selectedBugId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feedbackForm),
})
if (res.ok) {
notifications.show({
title: 'Success',
message: 'Feedback has been updated.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
refetch()
closeFeedbackModal()
setFeedbackForm({ feedBack: '' })
} else {
throw new Error('Failed to update feedback')
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} finally {
setIsUpdatingFeedback(false)
}
}
const handleUpdateStatus = async () => {
if (!selectedBugId || !updateForm.status) return
setIsUpdating(true)
try {
const res = await fetch(API_URLS.updateBugStatus(selectedBugId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateForm),
})
if (res.ok) {
notifications.show({
title: 'Success',
message: 'Status has been updated.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
refetch()
closeUpdateModal()
setUpdateForm({ status: '', description: '' })
} else {
throw new Error('Failed to update status')
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} finally {
setIsUpdating(false)
}
}
const handleCreateBug = async () => {
if (!createForm.description || !createForm.affectedVersion || !createForm.device || !createForm.os) {
notifications.show({
title: 'Validation Error',
message: 'Please fill in all required fields.',
color: 'red',
})
return
}
setIsSubmitting(true)
try {
const imageUrls: string[] = []
for (const file of imageFiles) {
const formData = new FormData()
formData.append('file', file)
const uploadRes = await fetch(API_URLS.uploadImage(), { method: 'POST', body: formData })
if (!uploadRes.ok) throw new Error('Gagal mengupload gambar')
const { url } = await uploadRes.json()
imageUrls.push(url)
}
const res = await fetch(API_URLS.createBug(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...createForm, imageUrls: imageUrls.length ? imageUrls : undefined }),
})
if (res.ok) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'CREATE', message: `Report error baru ditambahkan: ${createForm.description.substring(0, 50)}${createForm.description.length > 50 ? '...' : ''}` })
}).catch(console.error)
notifications.show({
title: 'Success',
message: 'Error report has been created.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
refetch()
close()
setImageFiles([])
setCreateForm({
description: '',
app: appId,
source: 'USER',
affectedVersion: '',
device: '',
os: '',
stackTrace: '',
})
} else {
throw new Error('Failed to create error report')
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} finally {
setIsSubmitting(false)
}
}
const bugs = data?.data || []
const totalPages = data?.totalPages || 1
return ( return (
<Stack gap="xl"> <Stack gap="xl">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
@@ -80,93 +263,472 @@ function AppErrorsPage() {
<Title order={3}>Error Reporting Center</Title> <Title order={3}>Error Reporting Center</Title>
<Text size="sm" c="dimmed">Advanced analysis of health issues and crashes for <b>{appId}</b>.</Text> <Text size="sm" c="dimmed">Advanced analysis of health issues and crashes for <b>{appId}</b>.</Text>
</Stack> </Stack>
<Button variant="light" color="red" leftSection={<TbBug size={16} />}> <Button
Export Logs variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
onClick={open}
>
Report Error
</Button> </Button>
</Group> </Group>
<Paper withBorder radius="2xl" className="glass" p="md"> {/* Image Preview Modal */}
<Group mb="md" grow> <Modal
opened={!!previewImage}
onClose={() => setPreviewImage(null)}
size="xl"
radius="xl"
padding={0}
withCloseButton={false}
overlayProps={{ backgroundOpacity: 0.75, blur: 6 }}
styles={{ content: { background: 'transparent', boxShadow: 'none' } }}
onClick={() => setPreviewImage(null)}
>
{previewImage && (
<Image
src={previewImage}
alt="Preview"
fit="contain"
style={{ maxHeight: '85vh', width: '100%' }}
/>
)}
</Modal>
<Modal
opened={updateModalOpened}
onClose={closeUpdateModal}
title={<Text fw={700} size="lg">Update Bug Status</Text>}
radius="xl"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Select
label="New Status"
placeholder="Select status"
required
data={[
{ value: 'OPEN', label: 'Open' },
{ value: 'ON_HOLD', label: 'On Hold' },
{ value: 'IN_PROGRESS', label: 'In Progress' },
{ value: 'RESOLVED', label: 'Resolved' },
{ value: 'RELEASED', label: 'Released' },
{ value: 'CLOSED', label: 'Closed' },
]}
value={updateForm.status}
onChange={(val) => setUpdateForm({ ...updateForm, status: val || '' })}
/>
<Textarea
label="Update Note (Optional)"
placeholder="E.g. Fixed in commit xxxxx / Assigned to team"
minRows={3}
value={updateForm.description}
onChange={(e) => setUpdateForm({ ...updateForm, description: e.target.value })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isUpdating}
onClick={handleUpdateStatus}
>
Save Changes
</Button>
</Stack>
</Modal>
<Modal
opened={feedbackModalOpened}
onClose={closeFeedbackModal}
title={<Text fw={700} size="lg">Developer Feedback</Text>}
radius="xl"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Textarea
data-autofocus
label="Feedback / Note"
placeholder="Explain the issue, root cause, or resolution..."
required
minRows={4}
value={feedbackForm.feedBack}
onChange={(e) => setFeedbackForm({ ...feedbackForm, feedBack: e.target.value })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isUpdatingFeedback}
onClick={handleUpdateFeedback}
>
Save Feedback
</Button>
</Stack>
</Modal>
<Modal
opened={opened}
onClose={() => { close(); setImageFiles([]); }}
title={<Text fw={700} size="lg">Report New Error</Text>}
radius="xl"
size="lg"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Textarea
label="Description"
placeholder="What happened? Describe the error in detail..."
required
minRows={3}
value={createForm.description}
onChange={(e) => setCreateForm({ ...createForm, description: e.target.value })}
/>
<SimpleGrid cols={2}>
<Select
label="Application"
data={appsList?.map((a: any) => ({ value: a.id, label: a.name })) || []}
value={createForm.app}
onChange={(val) => setCreateForm({ ...createForm, app: val as any })}
placeholder="Select application"
disabled={!appsList}
/>
<Select
label="Source"
data={[
{ value: 'USER', label: 'User' },
{ value: 'QC', label: 'QC' },
{ value: 'SYSTEM', label: 'System' },
]}
value={createForm.source}
onChange={(val) => setCreateForm({ ...createForm, source: val as any })}
/>
</SimpleGrid>
<TextInput <TextInput
placeholder="Search error message, village, or stack trace..." label="Version"
placeholder="e.g. 2.4.1"
required
value={createForm.affectedVersion}
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
/>
<SimpleGrid cols={2}>
<TextInput
label="Device"
placeholder="e.g. iPhone 13, Windows 11 PC"
required
value={createForm.device}
onChange={(e) => setCreateForm({ ...createForm, device: e.target.value })}
/>
<TextInput
label="OS"
placeholder="e.g. iOS 15.4, Windows 11"
required
value={createForm.os}
onChange={(e) => setCreateForm({ ...createForm, os: e.target.value })}
/>
</SimpleGrid>
<FileInput
label="Screenshot (Optional)"
placeholder="Klik untuk upload gambar..."
accept="image/*"
leftSection={<TbPhoto size={16} />}
description="Maks 3 gambar · 5MB per file · JPG, PNG, WEBP"
value={imageFiles}
onChange={(files) => {
if (files.length > 3) {
notifications.show({ title: 'Error', message: 'Maksimal 3 gambar', color: 'red' })
return
}
setImageFiles(files)
}}
clearable
multiple
/>
<Textarea
label="Stack Trace (Optional)"
placeholder="Paste code or error logs here..."
style={{ fontFamily: 'monospace' }}
minRows={2}
value={createForm.stackTrace}
onChange={(e) => setCreateForm({ ...createForm, stackTrace: e.target.value })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isSubmitting}
onClick={handleCreateBug}
>
Submit Error Report
</Button>
</Stack>
</Modal>
<Paper withBorder radius="2xl" className="glass" p="md">
<SimpleGrid cols={{ base: 1, sm: 3 }} mb="md">
<TextInput
placeholder="Search description, device, os..."
leftSection={<TbSearch size={16} />} leftSection={<TbSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
radius="md" radius="md"
/> />
<Select <Select
placeholder="Severity" placeholder="Status"
data={['Critical', 'High', 'Medium', 'Low']} data={[
leftSection={<TbFilter size={16} />} { value: 'all', label: 'All Status' },
{ value: 'OPEN', label: 'Open' },
{ value: 'ON_HOLD', label: 'On Hold' },
{ value: 'IN_PROGRESS', label: 'In Progress' },
{ value: 'RESOLVED', label: 'Resolved' },
{ value: 'RELEASED', label: 'Released' },
{ value: 'CLOSED', label: 'Closed' },
]}
value={status}
onChange={(val) => setStatus(val || 'all')}
radius="md" radius="md"
clearable
/> />
</Group> <Group justify="flex-end">
<Button variant="subtle" color="gray" leftSection={<TbFilter size={16} />} onClick={() => { setSearch(''); setStatus('all') }}>
Reset
</Button>
</Group>
</SimpleGrid>
<Accordion variant="separated" radius="xl"> {isLoading ? (
{mockErrors.map((error) => ( <Stack align="center" py="xl">
<Accordion.Item <Loader size="lg" type="dots" />
key={error.id} <Text size="sm" c="dimmed">Loading error reports...</Text>
value={error.id.toString()} </Stack>
style={{ border: '1px solid var(--mantine-color-default-border)', background: 'var(--mantine-color-default)', marginBottom: '12px' }} ) : bugs.length === 0 ? (
> <Paper p="xl" withBorder style={{ borderStyle: 'dashed', textAlign: 'center' }}>
<Accordion.Control> <TbBug size={48} color="gray" style={{ marginBottom: 12, opacity: 0.5 }} />
<Group wrap="nowrap"> <Text fw={600}>No error reports found</Text>
<ThemeIcon <Text size="sm" c="dimmed">Try adjusting your filters or search terms.</Text>
color={error.severity === 'critical' ? 'red' : error.severity === 'high' ? 'orange' : 'yellow'} </Paper>
variant="light" ) : (
size="lg" <Accordion variant="separated" radius="xl">
radius="md" {bugs.map((bug: any) => (
> <Accordion.Item
<TbAlertTriangle size={20} /> key={bug.id}
</ThemeIcon> value={bug.id}
<Box style={{ flex: 1 }}> style={{
<Group justify="space-between"> border: '1px solid var(--mantine-color-default-border)',
<Text size="sm" fw={600} lineClamp={1}>{error.title}</Text> background: 'var(--mantine-color-default)',
<Badge color={error.severity === 'critical' ? 'red' : 'orange'} variant="dot" size="xs"> marginBottom: '12px',
{error.severity.toUpperCase()} }}
</Badge> >
</Group> <Accordion.Control>
<Group gap="md"> <Group wrap="nowrap">
<Text size="xs" c="dimmed">{error.time} v{error.version}</Text> <ThemeIcon
<Group gap={4} visibleFrom="sm"> color={
<TbUserCheck size={12} color="gray" /> bug.status === 'OPEN'
<Text size="xs" c="dimmed">{error.users} Users Affected</Text> ? 'red'
: bug.status === 'IN_PROGRESS'
? 'blue'
: 'teal'
}
variant="light"
size="lg"
radius="md"
>
<TbAlertTriangle size={20} />
</ThemeIcon>
<Box style={{ flex: 1 }}>
<Group justify="space-between">
<Text size="sm" fw={600} lineClamp={1}>
{bug.description}
</Text>
<Badge
color={
bug.status === 'OPEN'
? 'red'
: bug.status === 'IN_PROGRESS'
? 'blue'
: 'teal'
}
variant="dot"
size="xs"
>
{bug.status}
</Badge>
</Group> </Group>
</Group> <Group gap="md">
</Box> <Text size="xs" c="dimmed">
</Group> {new Date(bug.createdAt).toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })} {bug.appId?.toUpperCase()} v{bug.affectedVersion}
</Accordion.Control> </Text>
<Accordion.Panel>
<Stack gap="md" py="xs">
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>MESSAGE</Text>
<Text size="sm" fw={500}>{error.message}</Text>
</Box>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVICE METADATA</Text>
<Group gap="xs">
{error.device.includes('PC') ? <TbDeviceDesktop size={14} color="gray" /> : <TbDeviceMobile size={14} color="gray" />}
<Text size="xs" fw={500}>{error.device}</Text>
</Group> </Group>
</Box> </Box>
</SimpleGrid>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>STACK TRACE</Text>
<Code block color="red" style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap', fontSize: '11px', border: '1px solid var(--mantine-color-default-border)' }}>
{error.stackTrace}
</Code>
</Box>
<Group justify="flex-end" pt="sm">
<Button variant="light" size="compact-xs" color="blue">Assign Developer</Button>
<Button variant="light" size="compact-xs" color="teal" leftSection={<TbCircleCheck size={14} />}>Mark as Fixed</Button>
</Group> </Group>
</Stack> </Accordion.Control>
</Accordion.Panel> <Accordion.Panel>
</Accordion.Item> <Stack gap="lg" py="xs">
))} {/* Device Info */}
</Accordion> <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVICE METADATA</Text>
<Group gap="xs">
{bug.device.toLowerCase().includes('windows') || bug.device.toLowerCase().includes('mac') || bug.device.toLowerCase().includes('pc') ? (
<TbDeviceDesktop size={14} color="gray" />
) : (
<TbDeviceMobile size={14} color="gray" />
)}
<Text size="xs" fw={500}>{bug.device} ({bug.os})</Text>
</Group>
</Box>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>SOURCE</Text>
<Badge variant="light" color="gray" size="sm">{bug.source}</Badge>
</Box>
</SimpleGrid>
{/* Feedback & Reporter Info */}
{(bug.user || bug.feedBack) && (
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
{bug.user && (
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>REPORTED BY</Text>
<Group gap="xs">
<Avatar src={bug.user.image} radius="xl" size="sm" color="blue">
{bug.user.name?.charAt(0).toUpperCase()}
</Avatar>
<Text size="sm">{bug.user.name}</Text>
</Group>
</Box>
)}
{bug.feedBack && (
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVELOPER FEEDBACK</Text>
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>{bug.feedBack}</Text>
</Box>
)}
</SimpleGrid>
)}
{/* Stack Trace */}
{bug.stackTrace && (
<Box>
<Group justify="space-between" mb={4}>
<Text size="xs" fw={700} c="dimmed">STACK TRACE</Text>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => toggleStackTrace(bug.id)}
>
{showStackTrace[bug.id] ? 'Hide' : 'Show'}
</Button>
</Group>
<Collapse in={!!showStackTrace[bug.id]}>
<Code
block
color="red"
style={{
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
fontSize: '11px',
border: '1px solid var(--mantine-color-default-border)',
}}
>
{bug.stackTrace}
</Code>
</Collapse>
</Box>
)}
{/* Images */}
{bug.images && bug.images.length > 0 && (
<Box>
<Group gap="xs" mb={8}>
<TbPhoto size={16} color="gray" />
<Text size="xs" fw={700} c="dimmed">ATTACHED IMAGES ({bug.images.length})</Text>
</Group>
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
{bug.images.map((img: any) => (
<Paper
key={img.id}
withBorder
radius="md"
style={{ overflow: 'hidden', cursor: 'zoom-in' }}
onClick={() => setPreviewImage(img.imageUrl)}
>
<Image src={img.imageUrl} alt="Error screenshot" height={100} fit="cover" />
</Paper>
))}
</SimpleGrid>
</Box>
)}
{/* Logs / History */}
{bug.logs && bug.logs.length > 0 && (
<Box>
<Group justify="space-between" mb={showLogs[bug.id] ? 12 : 0}>
<Group gap="xs">
<TbHistory size={16} color="gray" />
<Text size="xs" fw={700} c="dimmed">ACTIVITY LOG ({bug.logs.length})</Text>
</Group>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => toggleLogs(bug.id)}
>
{showLogs[bug.id] ? 'Hide' : 'Show'}
</Button>
</Group>
<Collapse in={showLogs[bug.id]}>
<Timeline active={bug.logs.length - 1} bulletSize={24} lineWidth={2} mt="md">
{bug.logs.map((log: any) => (
<Timeline.Item
key={log.id}
bullet={
<Badge size="xs" circle color={log.status === 'RESOLVED' ? 'teal' : 'blue'}> </Badge>
}
title={<Text size="sm" fw={600}>{log.status}</Text>}
>
<Text size="xs" c="dimmed" mb={4}>
{new Date(log.createdAt).toLocaleString()} by {log.user?.name || 'Unknown'}
</Text>
<Text size="sm">{log.description}</Text>
</Timeline.Item>
))}
</Timeline>
</Collapse>
</Box>
)}
<Group justify="flex-end" pt="sm">
<Button variant="light" size="compact-xs" color="blue" onClick={() => {
setSelectedBugId(bug.id)
setFeedbackForm({ feedBack: bug.feedBack || '' })
openFeedbackModal()
}}>Developer Feedback</Button>
<Button variant="light" size="compact-xs" color="teal" onClick={() => {
setSelectedBugId(bug.id)
setUpdateForm({ status: bug.status, description: '' })
openUpdateModal()
}}>Update Status</Button>
</Group>
</Stack>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
)}
{totalPages > 1 && (
<Group justify="center" mt="xl">
<Pagination total={totalPages} value={page} onChange={setPage} radius="xl" />
</Group>
)}
</Paper> </Paper>
</Stack> </Stack>
) )

View File

@@ -1,33 +1,32 @@
import { useEffect, useState } from 'react' import { useQuery } from '@tanstack/react-query'
import { notifications } from '@mantine/notifications'
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'
import { useSession } from '@/frontend/hooks/useAuth'
import { import {
ActionIcon, Badge,
Button,
Group, Group,
Modal,
SimpleGrid, SimpleGrid,
Stack, Stack,
Text,
Title,
Modal,
Button,
TextInput,
Switch, Switch,
Badge, Text,
Textarea, Textarea,
Skeleton TextInput,
Title
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router' import { notifications } from '@mantine/notifications'
import useSWR from 'swr' import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { import {
TbActivity, TbActivity,
TbAlertTriangle, TbAlertTriangle,
TbBuildingCommunity, TbBuildingCommunity,
TbRefresh,
TbVersions TbVersions
} from 'react-icons/tb' } from 'react-icons/tb'
import useSWR from 'swr'
import { API_URLS } from '../config/api' import { API_URLS } from '../config/api'
export const Route = createFileRoute('/apps/$appId/')({ export const Route = createFileRoute('/apps/$appId/')({
@@ -41,6 +40,8 @@ function AppOverviewPage() {
const navigate = useNavigate() const navigate = useNavigate()
const isDesaPlus = appId === 'desa-plus' const isDesaPlus = appId === 'desa-plus'
const [versionModalOpened, { open: openVersionModal, close: closeVersionModal }] = useDisclosure(false) const [versionModalOpened, { open: openVersionModal, close: closeVersionModal }] = useDisclosure(false)
const { data: session } = useSession()
const isDeveloper = session?.user?.role === 'DEVELOPER'
// Form State // Form State
const [latestVersion, setLatestVersion] = useState('') const [latestVersion, setLatestVersion] = useState('')
@@ -54,6 +55,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 || []
@@ -89,6 +95,12 @@ function AppOverviewPage() {
}) })
if (response.ok) { if (response.ok) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'UPDATE', message: `Update version information: ${JSON.stringify({ latestVersion, minVersion, maintenance, messageUpdate })}` })
}).catch(console.error)
notifications.show({ notifications.show({
title: 'Update Successful', title: 'Update Successful',
message: 'Application version information has been updated.', message: 'Application version information has been updated.',
@@ -118,29 +130,29 @@ function AppOverviewPage() {
<> <>
<Modal opened={versionModalOpened} onClose={closeVersionModal} title="Update Version Information" radius="md"> <Modal opened={versionModalOpened} onClose={closeVersionModal} title="Update Version Information" radius="md">
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
label="Active Version" label="Active Version"
placeholder="e.g. 2.0.5" placeholder="e.g. 2.0.5"
value={latestVersion} value={latestVersion}
onChange={(e) => setLatestVersion(e.currentTarget.value)} onChange={(e) => setLatestVersion(e.currentTarget.value)}
/> />
<TextInput <TextInput
label="Minimum Version" label="Minimum Version"
placeholder="e.g. 2.0.0" placeholder="e.g. 2.0.0"
value={minVersion} value={minVersion}
onChange={(e) => setMinVersion(e.currentTarget.value)} onChange={(e) => setMinVersion(e.currentTarget.value)}
/> />
<Textarea <Textarea
label="Update Message" label="Update Message"
placeholder="Enter release notes or update message..." placeholder="Enter release notes or update message..."
value={messageUpdate} value={messageUpdate}
onChange={(e) => setMessageUpdate(e.currentTarget.value)} onChange={(e) => setMessageUpdate(e.currentTarget.value)}
minRows={3} minRows={3}
autosize autosize
/> />
<Switch <Switch
label="Maintenance Mode" label="Maintenance Mode"
description="Enable to put the app in maintenance mode for users." description="Enable to put the app in maintenance mode for users."
checked={maintenance} checked={maintenance}
onChange={(e) => setMaintenance(e.currentTarget.checked)} onChange={(e) => setMaintenance(e.currentTarget.checked)}
/> />
@@ -168,7 +180,7 @@ function AppOverviewPage() {
value={gridLoading ? '...' : (grid?.version?.mobile_latest_version || 'N/A')} value={gridLoading ? '...' : (grid?.version?.mobile_latest_version || 'N/A')}
icon={TbVersions} icon={TbVersions}
color="brand-blue" color="brand-blue"
onClick={openVersionModal} onClick={isDeveloper ? openVersionModal : undefined}
> >
<Group justify="space-between" mt="md"> <Group justify="space-between" mt="md">
<Stack gap={0}> <Stack gap={0}>
@@ -206,12 +218,12 @@ 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 }} onClick={() => navigate({ to: `/apps/${appId}/errors` })}
/> />
</SimpleGrid> </SimpleGrid>
@@ -220,7 +232,7 @@ function AppOverviewPage() {
<VillageComparisonBarChart data={comparisonData} isLoading={comparisonLoading} /> <VillageComparisonBarChart data={comparisonData} isLoading={comparisonLoading} />
</SimpleGrid> </SimpleGrid>
<ErrorDataTable /> <ErrorDataTable appId={appId} />
</Stack> </Stack>
</> </>
) )

View File

@@ -172,10 +172,10 @@ function AppLogsPage() {
> >
<Table.Thead bg="rgba(0,0,0,0.05)"> <Table.Thead bg="rgba(0,0,0,0.05)">
<Table.Tr> <Table.Tr>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '18%' }}>Timestamp</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '15%' }}>Timestamp</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '20%' }}>User & Village</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '20%' }}>User & Village</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '12%' }}>Action</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '15%' }}>Action</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '50%' }}>Description</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '40%' }}>Description</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
@@ -186,14 +186,18 @@ function AppLogsPage() {
<ThemeIcon variant="transparent" color="gray" size="sm"> <ThemeIcon variant="transparent" color="gray" size="sm">
<TbCalendar size={14} /> <TbCalendar size={14} />
</ThemeIcon> </ThemeIcon>
<Stack gap={0}> {log.createdAt.endsWith('lalu') ? (
<Text size="xs" fw={700} style={{ color: 'var(--mantine-color-white)' }}> <Text size="xs" fw={700}>{log.createdAt}</Text>
{log.createdAt.split(' ').slice(1).join(' ')} ) : (
</Text> <Stack gap={0}>
<Text size="xs" c="dimmed"> <Text size="xs" fw={700}>
{log.createdAt.split(' ')[0]} {log.createdAt.split(' ').slice(1).join(' ')}
</Text> </Text>
</Stack> <Text size="xs" c="dimmed">
{log.createdAt.split(' ')[0]}
</Text>
</Stack>
)}
</Group> </Group>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
@@ -216,7 +220,10 @@ function AppLogsPage() {
color={getActionColor(log.action)} color={getActionColor(log.action)}
radius="sm" radius="sm"
size="xs" size="xs"
styles={{ root: { fontWeight: 800 } }} styles={{
root: { fontWeight: 800 },
label: { textOverflow: 'clip', overflow: 'visible' }
}}
> >
{log.action} {log.action}
</Badge> </Badge>

View File

@@ -169,6 +169,12 @@ function UsersIndexPage() {
const result = await res.json() const result = await res.json()
if (result.success) { if (result.success) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'CREATE', message: `Didaftarkan user (${appId}) baru: ${form.name}-${form.nik}` })
}).catch(console.error)
notifications.show({ notifications.show({
title: 'Success', title: 'Success',
message: 'User has been created successfully.', message: 'User has been created successfully.',
@@ -252,6 +258,12 @@ function UsersIndexPage() {
const result = await res.json() const result = await res.json()
if (result.success) { if (result.success) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'UPDATE', message: `Data user (${appId}) diperbarui: ${editForm.name}-${editForm.id}` })
}).catch(console.error)
notifications.show({ notifications.show({
title: 'Success', title: 'Success',
message: 'User has been updated successfully.', message: 'User has been updated successfully.',
@@ -639,8 +651,8 @@ function UsersIndexPage() {
<Paper withBorder radius="2xl" className="glass" style={{ overflow: 'hidden' }}> <Paper withBorder radius="2xl" className="glass" style={{ overflow: 'hidden' }}>
<ScrollArea h={isMobile ? undefined : 'auto'} offsetScrollbars> <ScrollArea h={isMobile ? undefined : 'auto'} offsetScrollbars>
<Table <Table
verticalSpacing="lg" verticalSpacing="md"
horizontalSpacing="xl" horizontalSpacing="md"
highlightOnHover highlightOnHover
withColumnBorders={false} withColumnBorders={false}
style={{ style={{
@@ -654,14 +666,13 @@ function UsersIndexPage() {
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '28%' }}>User & ID</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '28%' }}>User & ID</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '25%' }}>Contact Detail</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '25%' }}>Contact Detail</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '22%' }}>Organization</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '22%' }}>Organization</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '12%' }}>Role</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '20%' }}>Role</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '10%' }}>Status</Table.Th> <Table.Th style={{ border: 'none', width: isMobile ? undefined : '10%' }}>Status</Table.Th>
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '8%' }}>Actions</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{users.map((user) => ( {users.map((user) => (
<Table.Tr key={user.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}> <Table.Tr key={user.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }} onClick={()=>{handleEditOpen(user)}}>
<Table.Td> <Table.Td>
<Group gap="md" wrap="nowrap"> <Group gap="md" wrap="nowrap">
<Avatar <Avatar
@@ -745,17 +756,6 @@ function UsersIndexPage() {
)} )}
</Stack> </Stack>
</Table.Td> </Table.Td>
<Table.Td>
<ActionIcon
variant="light"
color="brand-blue"
onClick={() => handleEditOpen(user)}
size="md"
radius="md"
>
<TbEdit size={18} />
</ActionIcon>
</Table.Td>
</Table.Tr> </Table.Tr>
))} ))}
</Table.Tbody> </Table.Tbody>

View File

@@ -4,14 +4,19 @@ import {
Button, Button,
Card, Card,
Group, Group,
Modal,
Paper, Paper,
SegmentedControl, SegmentedControl,
SimpleGrid, SimpleGrid,
Stack, Stack,
Text, Text,
Textarea,
TextInput,
ThemeIcon, ThemeIcon,
Title Title
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { useState } from 'react' import { useState } from 'react'
import { import {
@@ -32,6 +37,7 @@ import {
} from 'react-icons/tb' } from 'react-icons/tb'
import useSWR from 'swr' import useSWR from 'swr'
import { API_URLS } from '../config/api' import { API_URLS } from '../config/api'
import { useSession } from '../hooks/useAuth'
const fetcher = (url: string) => fetch(url).then((res) => res.json()) const fetcher = (url: string) => fetch(url).then((res) => res.json())
@@ -67,7 +73,14 @@ function ActivityChart({ villageId }: { villageId: string }) {
yearly: 'Yearly', yearly: 'Yearly',
} }
const data = response?.data || [] const rawData: any[] = Array.isArray(response?.data) ? response.data : []
// Normalize: map any field names from external API → { label, activity }
const data = rawData.map((item) => {
const label = item.label
const activity = item.aktivitas
return { label: String(label), activity: Number(activity) }
})
return ( return (
<Paper withBorder radius="xl" p="lg"> <Paper withBorder radius="xl" p="lg">
@@ -104,35 +117,20 @@ function ActivityChart({ villageId }: { villageId: string }) {
h={280} h={280}
data={data} data={data}
dataKey="label" dataKey="label"
series={[{ name: 'aktivitas', color: '#2563EB', label: 'Activity' }]} series={[{ name: 'activity', color: '#2563EB' }]}
curveType="monotone" curveType="monotone"
withTooltip withTooltip={true}
withDots withDots={true}
tickLine="none" withPointLabels={false}
gridAxis="x"
tooltipAnimationDuration={150} tooltipAnimationDuration={150}
fillOpacity={1} tooltipProps={{
areaProps={{ allowEscapeViewBox: { x: true, y: false },
strokeWidth: 2.5,
fill: 'url(#villageAreaGrad)',
stroke: '#2563EB',
filter: 'drop-shadow(0 4px 12px rgba(37,99,235,0.3))',
}} }}
dotProps={{ activeDotProps={{
r: 4, r: 6,
strokeWidth: 2, strokeWidth: 2,
stroke: '#2563EB',
fill: 'white',
}} }}
> />
<defs>
<linearGradient id="villageAreaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#2563EB" stopOpacity={0.35} />
<stop offset="75%" stopColor="#7C3AED" stopOpacity={0.08} />
<stop offset="100%" stopColor="#7C3AED" stopOpacity={0} />
</linearGradient>
</defs>
</AreaChart>
)} )}
</Paper> </Paper>
) )
@@ -144,12 +142,135 @@ function VillageDetailPage() {
const { appId, villageId } = useParams({ from: '/apps/$appId/villages/$villageId' }) const { appId, villageId } = useParams({ from: '/apps/$appId/villages/$villageId' })
const navigate = useNavigate() const navigate = useNavigate()
const { data: infoRes, isLoading: infoLoading } = useSWR(API_URLS.infoVillages(villageId), fetcher) const { data: session } = useSession()
const isDeveloper = session?.user?.role === 'DEVELOPER'
const { data: infoRes, isLoading: infoLoading, mutate } = useSWR(API_URLS.infoVillages(villageId), fetcher)
const { data: gridRes, isLoading: gridLoading } = useSWR(API_URLS.gridVillages(villageId), fetcher) const { data: gridRes, isLoading: gridLoading } = useSWR(API_URLS.gridVillages(villageId), fetcher)
const [confirmModalOpened, { open: openConfirmModal, close: closeConfirmModal }] = useDisclosure(false)
const [editModalOpened, { open: openEditModal, close: closeEditModal }] = useDisclosure(false)
const [isUpdating, setIsUpdating] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [editForm, setEditForm] = useState({ name: '', desc: '' })
const village = infoRes?.data const village = infoRes?.data
const stats = gridRes?.data const stats = gridRes?.data
const openEdit = () => {
setEditForm({
name: village?.name || '',
desc: village?.desc || ''
})
openEditModal()
}
const handleEditVillage = async () => {
if (!village) return
if (!editForm.name.trim() || !editForm.desc.trim()) {
notifications.show({
title: 'Validation Error',
message: 'All fields are required.',
color: 'red'
})
return
}
setIsEditing(true)
try {
const res = await fetch(API_URLS.editVillages(), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: village.id,
name: editForm.name,
desc: editForm.desc
})
})
if (res.ok) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'UPDATE', message: `Data desa (${appId}) diperbarui: ${editForm.name}-${village.id}` })
}).catch(console.error)
notifications.show({
title: 'Success',
message: 'Village data has been updated successfully.',
color: 'teal'
})
mutate()
closeEditModal()
} else {
notifications.show({
title: 'Error',
message: 'Failed to update village data.',
color: 'red'
})
}
} catch (error) {
notifications.show({
title: 'Error',
message: 'A network error occurred.',
color: 'red'
})
} finally {
setIsEditing(false)
}
}
const handleConfirmToggle = async () => {
if (!village) return
setIsUpdating(true)
try {
const res = await fetch(API_URLS.updateStatusVillages(), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: village.id,
active: !village.isActive
})
})
if (res.ok) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'UPDATE', message: `Status desa (${appId}) diperbarui (${!village.isActive ? 'activated' : 'deactivated'}): ${village.name}-${village.id}` })
}).catch(console.error)
notifications.show({
title: 'Success',
message: `Village status has been ${!village.isActive ? 'activated' : 'deactivated'}.`,
color: 'teal'
})
mutate()
closeConfirmModal()
} else {
notifications.show({
title: 'Error',
message: 'Failed to update village status.',
color: 'red'
})
}
} catch (error) {
notifications.show({
title: 'Error',
message: 'A network error occurred.',
color: 'red'
})
} finally {
setIsUpdating(false)
}
}
const goBack = () => navigate({ to: '/apps/$appId/villages', params: { appId } }) const goBack = () => navigate({ to: '/apps/$appId/villages', params: { appId } })
if (infoLoading || gridLoading) { if (infoLoading || gridLoading) {
@@ -195,8 +316,10 @@ function VillageDetailPage() {
variant="filled" variant="filled"
color={village.isActive ? 'red' : 'green'} color={village.isActive ? 'red' : 'green'}
leftSection={village.isActive ? <TbPower size={16} /> : <TbPower size={16} />} leftSection={village.isActive ? <TbPower size={16} /> : <TbPower size={16} />}
onClick={() => alert(`Toggle status for ${village.name}`)} onClick={openConfirmModal}
radius="md" radius="md"
loading={isUpdating}
disabled={!isDeveloper}
> >
{village.isActive ? 'Deactivate' : 'Active'} {village.isActive ? 'Deactivate' : 'Active'}
</Button> </Button>
@@ -204,7 +327,7 @@ function VillageDetailPage() {
variant="light" variant="light"
color="blue" color="blue"
leftSection={<TbEdit size={16} />} leftSection={<TbEdit size={16} />}
onClick={() => alert(`Edit settings for ${village.name}`)} onClick={openEdit}
radius="md" radius="md"
> >
Edit Edit
@@ -314,7 +437,9 @@ function VillageDetailPage() {
}} }}
> >
{/* Left (3/4): Activity Chart */} {/* Left (3/4): Activity Chart */}
<ActivityChart villageId={villageId} /> <Box style={{ minWidth: 0 }}>
<ActivityChart villageId={villageId} />
</Box>
{/* Right (1/4): Informasi Sistem */} {/* Right (1/4): Informasi Sistem */}
<Paper withBorder radius="xl" p="lg"> <Paper withBorder radius="xl" p="lg">
@@ -328,7 +453,7 @@ function VillageDetailPage() {
{[ {[
{ label: 'Date Created', value: village.createdAt }, { label: 'Date Created', value: village.createdAt },
{ label: 'Created By', value: '-' }, { label: 'Created By', value: '-' },
{ label: 'Last Updated', value: '-' }, { label: 'Last Updated', value: village.updatedAt },
].map((item, idx, arr) => ( ].map((item, idx, arr) => (
<Group <Group
key={item.label} key={item.label}
@@ -347,6 +472,75 @@ function VillageDetailPage() {
</Paper> </Paper>
</Box> </Box>
{/* ── Confirmation Modal ── */}
<Modal
opened={confirmModalOpened}
onClose={closeConfirmModal}
title={<Text fw={700}>Confirm Status Change</Text>}
radius="xl"
centered
>
<Stack gap="md">
<Text size="sm">
Are you sure you want to <strong>{village.isActive ? 'deactivate' : 'activate'}</strong> village <strong>{village.name}</strong>?
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="light" color="gray" onClick={closeConfirmModal} radius="md">
Cancel
</Button>
<Button
color={village.isActive ? 'red' : 'green'}
onClick={handleConfirmToggle}
loading={isUpdating}
radius="md"
>
Confirm
</Button>
</Group>
</Stack>
</Modal>
{/* ── Edit Village Modal ── */}
<Modal
opened={editModalOpened}
onClose={closeEditModal}
title={<Text fw={700}>Edit Village Details</Text>}
radius="xl"
size="md"
>
<Stack gap="md">
<TextInput
label="Village Name"
placeholder="Enter village name"
required
value={editForm.name}
onChange={(e) => setEditForm(prev => ({ ...prev, name: e.currentTarget.value }))}
/>
<Textarea
label="Description"
placeholder="Enter village description..."
minRows={3}
required
value={editForm.desc}
onChange={(e) => setEditForm(prev => ({ ...prev, desc: e.currentTarget.value }))}
/>
<Group justify="flex-end" gap="sm" mt="md">
<Button variant="light" color="gray" onClick={closeEditModal} radius="md">
Cancel
</Button>
<Button
variant="filled"
color="blue"
onClick={handleEditVillage}
loading={isEditing}
radius="md"
>
Save Changes
</Button>
</Group>
</Stack>
</Modal>
</Stack> </Stack>
) )
} }

View File

@@ -283,6 +283,12 @@ function AppVillagesIndexPage() {
}) })
if (res.ok) { if (res.ok) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'CREATE', message: `Desa baru didaftarkan: ${form.name}` })
}).catch(console.error)
notifications.show({ notifications.show({
title: 'Success', title: 'Success',
message: 'Village has been successfully registered.', message: 'Village has been successfully registered.',

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

@@ -0,0 +1,756 @@
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { API_URLS } from '@/frontend/config/api'
import {
Accordion,
Avatar,
Badge,
Box,
Button,
Code,
Collapse,
Container,
Group,
Image,
Loader,
Modal,
Pagination,
Paper,
Select,
SimpleGrid,
Stack,
Text,
ThemeIcon,
FileInput,
TextInput,
Textarea,
Title,
Timeline,
} from '@mantine/core'
import { useQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import {
TbAlertTriangle,
TbBug,
TbDeviceDesktop,
TbDeviceMobile,
TbFilter,
TbSearch,
TbHistory,
TbPhoto,
TbPlus,
TbCircleCheck,
TbCircleX,
} from 'react-icons/tb'
export const Route = createFileRoute('/bug-reports')({
component: ListErrorsPage,
})
function ListErrorsPage() {
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const [app, setApp] = useState('all')
const [status, setStatus] = useState('all')
const [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
const [showStackTrace, setShowStackTrace] = useState<Record<string, boolean>>({})
const toggleLogs = (bugId: string) => {
setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
}
const toggleStackTrace = (bugId: string) => {
setShowStackTrace((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
}
const { data, isLoading, refetch } = useQuery({
queryKey: ['bugs', { page, search, app, status }],
queryFn: () =>
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()),
})
// Image Preview
const [previewImage, setPreviewImage] = useState<string | null>(null)
// Create Bug Modal Logic
const [opened, { open, close }] = useDisclosure(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [imageFiles, setImageFiles] = useState<File[]>([])
const [createForm, setCreateForm] = useState({
description: '',
app: 'desa-plus',
source: 'USER',
affectedVersion: '',
device: '',
os: '',
stackTrace: '',
})
// Update Status Modal Logic
const [updateModalOpened, { open: openUpdateModal, close: closeUpdateModal }] = useDisclosure(false)
const [isUpdating, setIsUpdating] = useState(false)
const [selectedBugId, setSelectedBugId] = useState<string | null>(null)
const [updateForm, setUpdateForm] = useState({
status: '',
description: '',
})
// Feedback Modal Logic
const [feedbackModalOpened, { open: openFeedbackModal, close: closeFeedbackModal }] = useDisclosure(false)
const [isUpdatingFeedback, setIsUpdatingFeedback] = useState(false)
const [feedbackForm, setFeedbackForm] = useState({
feedBack: '',
})
const handleUpdateFeedback = async () => {
if (!selectedBugId || !feedbackForm.feedBack) return
setIsUpdatingFeedback(true)
try {
const res = await fetch(API_URLS.updateBugFeedback(selectedBugId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feedbackForm),
})
if (res.ok) {
notifications.show({
title: 'Success',
message: 'Feedback has been updated.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
refetch()
closeFeedbackModal()
setFeedbackForm({ feedBack: '' })
} else {
throw new Error('Failed to update feedback')
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} finally {
setIsUpdatingFeedback(false)
}
}
const handleUpdateStatus = async () => {
if (!selectedBugId || !updateForm.status) return
setIsUpdating(true)
try {
const res = await fetch(API_URLS.updateBugStatus(selectedBugId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateForm),
})
if (res.ok) {
notifications.show({
title: 'Success',
message: 'Status has been updated.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
refetch()
closeUpdateModal()
setUpdateForm({ status: '', description: '' })
} else {
throw new Error('Failed to update status')
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} finally {
setIsUpdating(false)
}
}
const handleCreateBug = async () => {
if (!createForm.description || !createForm.affectedVersion || !createForm.device || !createForm.os) {
notifications.show({
title: 'Validation Error',
message: 'Please fill in all required fields.',
color: 'red',
})
return
}
setIsSubmitting(true)
try {
const imageUrls: string[] = []
for (const file of imageFiles) {
const formData = new FormData()
formData.append('file', file)
const uploadRes = await fetch(API_URLS.uploadImage(), { method: 'POST', body: formData })
if (!uploadRes.ok) throw new Error('Gagal mengupload gambar')
const { url } = await uploadRes.json()
imageUrls.push(url)
}
const res = await fetch(API_URLS.createBug(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...createForm, imageUrls: imageUrls.length ? imageUrls : undefined }),
})
if (res.ok) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'CREATE', message: `Report error baru ditambahkan: ${createForm.description.substring(0, 50)}${createForm.description.length > 50 ? '...' : ''}` })
}).catch(console.error)
notifications.show({
title: 'Success',
message: 'Error report has been created.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
refetch()
close()
setImageFiles([])
setCreateForm({
description: '',
app: 'desa-plus',
source: 'USER',
affectedVersion: '',
device: '',
os: '',
stackTrace: '',
})
} else {
throw new Error('Failed to create error report')
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} finally {
setIsSubmitting(false)
}
}
const bugs = data?.data || []
const totalPages = data?.totalPages || 1
return (
<DashboardLayout>
<Container size="xl" py="lg">
<Stack gap="xl">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Title order={2} className="gradient-text">
Error Reports
</Title>
<Text size="sm" c="dimmed">
Centralized error tracking and analysis for all applications.
</Text>
</Stack>
<Group>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
onClick={open}
>
Report Error
</Button>
{/* <Button variant="light" color="red" leftSection={<TbBug size={16} />}>
Generate Report
</Button> */}
</Group>
</Group>
{/* Image Preview Modal */}
<Modal
opened={!!previewImage}
onClose={() => setPreviewImage(null)}
size="xl"
radius="xl"
padding={0}
withCloseButton={false}
overlayProps={{ backgroundOpacity: 0.75, blur: 6 }}
styles={{ content: { background: 'transparent', boxShadow: 'none' } }}
onClick={() => setPreviewImage(null)}
>
{previewImage && (
<Image
src={previewImage}
alt="Preview"
fit="contain"
style={{ maxHeight: '85vh', width: '100%' }}
/>
)}
</Modal>
<Modal
opened={updateModalOpened}
onClose={closeUpdateModal}
title={<Text fw={700} size="lg">Update Bug Status</Text>}
radius="xl"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Select
label="New Status"
placeholder="Select status"
required
data={[
{ value: 'OPEN', label: 'Open' },
{ value: 'ON_HOLD', label: 'On Hold' },
{ value: 'IN_PROGRESS', label: 'In Progress' },
{ value: 'RESOLVED', label: 'Resolved' },
{ value: 'RELEASED', label: 'Released' },
{ value: 'CLOSED', label: 'Closed' },
]}
value={updateForm.status}
onChange={(val) => setUpdateForm({ ...updateForm, status: val || '' })}
/>
<Textarea
label="Update Note (Optional)"
placeholder="E.g. Fixed in commit xxxxx / Assigned to team"
minRows={3}
value={updateForm.description}
onChange={(e) => setUpdateForm({ ...updateForm, description: e.target.value })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isUpdating}
onClick={handleUpdateStatus}
>
Save Changes
</Button>
</Stack>
</Modal>
<Modal
opened={feedbackModalOpened}
onClose={closeFeedbackModal}
title={<Text fw={700} size="lg">Developer Feedback</Text>}
radius="xl"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Textarea
data-autofocus
label="Feedback / Note"
placeholder="Explain the issue, root cause, or resolution..."
required
minRows={4}
value={feedbackForm.feedBack}
onChange={(e) => setFeedbackForm({ ...feedbackForm, feedBack: e.target.value })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isUpdatingFeedback}
onClick={handleUpdateFeedback}
>
Save Feedback
</Button>
</Stack>
</Modal>
<Modal
opened={opened}
onClose={() => { close(); setImageFiles([]); }}
title={<Text fw={700} size="lg">Report New Error</Text>}
radius="xl"
size="lg"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Textarea
label="Description"
placeholder="What happened? Describe the error in detail..."
required
minRows={3}
value={createForm.description}
onChange={(e) => setCreateForm({ ...createForm, description: e.target.value })}
/>
<SimpleGrid cols={2}>
<Select
label="Application"
data={appsList?.map((a: any) => ({ value: a.id, label: a.name })) || []}
value={createForm.app}
onChange={(val) => setCreateForm({ ...createForm, app: val as any })}
placeholder="Select application"
disabled={!appsList}
/>
<Select
label="Source"
data={[
{ value: 'USER', label: 'User' },
{ value: 'QC', label: 'QC' },
{ value: 'SYSTEM', label: 'System' },
]}
value={createForm.source}
onChange={(val) => setCreateForm({ ...createForm, source: val as any })}
/>
</SimpleGrid>
<TextInput
label="Version"
placeholder="e.g. 2.4.1"
required
value={createForm.affectedVersion}
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
/>
<SimpleGrid cols={2}>
<TextInput
label="Device"
placeholder="e.g. iPhone 13, Windows 11 PC"
required
value={createForm.device}
onChange={(e) => setCreateForm({ ...createForm, device: e.target.value })}
/>
<TextInput
label="OS"
placeholder="e.g. iOS 15.4, Windows 11"
required
value={createForm.os}
onChange={(e) => setCreateForm({ ...createForm, os: e.target.value })}
/>
</SimpleGrid>
<FileInput
label="Screenshot (Optional)"
placeholder="Klik untuk upload gambar..."
accept="image/*"
leftSection={<TbPhoto size={16} />}
description="Maks 3 gambar · 5MB per file · JPG, PNG, WEBP"
value={imageFiles}
onChange={(files) => {
if (files.length > 3) {
notifications.show({ title: 'Error', message: 'Maksimal 3 gambar', color: 'red' })
return
}
setImageFiles(files)
}}
clearable
multiple
/>
<Textarea
label="Stack Trace (Optional)"
placeholder="Paste code or error logs here..."
style={{ fontFamily: 'monospace' }}
minRows={2}
value={createForm.stackTrace}
onChange={(e) => setCreateForm({ ...createForm, stackTrace: e.target.value })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isSubmitting}
onClick={handleCreateBug}
>
Submit Error Report
</Button>
</Stack>
</Modal>
<Paper withBorder radius="2xl" className="glass" p="md">
<SimpleGrid cols={{ base: 1, sm: 4 }} mb="md">
<TextInput
placeholder="Search description, device, os..."
leftSection={<TbSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
radius="md"
/>
<Select
placeholder="Application"
data={[
{ value: 'all', label: 'All Applications' },
...(appsList?.map((a: any) => ({ value: a.id, label: a.name })) || []),
]}
value={app}
onChange={(val) => setApp(val || 'all')}
radius="md"
disabled={!appsList}
/>
<Select
placeholder="Status"
data={[
{ value: 'all', label: 'All Status' },
{ value: 'OPEN', label: 'Open' },
{ value: 'ON_HOLD', label: 'On Hold' },
{ value: 'IN_PROGRESS', label: 'In Progress' },
{ value: 'RESOLVED', label: 'Resolved' },
{ value: 'RELEASED', label: 'Released' },
{ value: 'CLOSED', label: 'Closed' },
]}
value={status}
onChange={(val) => setStatus(val || 'all')}
radius="md"
/>
<Group justify="flex-end">
<Button variant="subtle" color="gray" leftSection={<TbFilter size={16} />} onClick={() => {setSearch(''); setApp('all'); setStatus('all')}}>
Reset
</Button>
</Group>
</SimpleGrid>
{isLoading ? (
<Stack align="center" py="xl">
<Loader size="lg" type="dots" />
<Text size="sm" c="dimmed">Loading error reports...</Text>
</Stack>
) : bugs.length === 0 ? (
<Paper p="xl" withBorder style={{ borderStyle: 'dashed', textAlign: 'center' }}>
<TbBug size={48} color="gray" style={{ marginBottom: 12, opacity: 0.5 }} />
<Text fw={600}>No error reports found</Text>
<Text size="sm" c="dimmed">Try adjusting your filters or search terms.</Text>
</Paper>
) : (
<Accordion variant="separated" radius="xl">
{bugs.map((bug: any) => (
<Accordion.Item
key={bug.id}
value={bug.id}
style={{
border: '1px solid var(--mantine-color-default-border)',
background: 'var(--mantine-color-default)',
marginBottom: '12px',
}}
>
<Accordion.Control>
<Group wrap="nowrap">
<ThemeIcon
color={
bug.status === 'OPEN'
? 'red'
: bug.status === 'IN_PROGRESS'
? 'blue'
: 'teal'
}
variant="light"
size="lg"
radius="md"
>
<TbAlertTriangle size={20} />
</ThemeIcon>
<Box style={{ flex: 1 }}>
<Group justify="space-between">
<Text size="sm" fw={600} lineClamp={1}>
{bug.description}
</Text>
<Badge
color={
bug.status === 'OPEN'
? 'red'
: bug.status === 'IN_PROGRESS'
? 'blue'
: 'teal'
}
variant="dot"
size="xs"
>
{bug.status}
</Badge>
</Group>
<Group gap="md">
<Text size="xs" c="dimmed">
{new Date(bug.createdAt).toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })} {bug.appId?.toUpperCase()} v{bug.affectedVersion}
</Text>
</Group>
</Box>
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap="lg" py="xs">
{/* Device Info */}
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVICE METADATA</Text>
<Group gap="xs">
{bug.device.toLowerCase().includes('windows') || bug.device.toLowerCase().includes('mac') || bug.device.toLowerCase().includes('pc') ? (
<TbDeviceDesktop size={14} color="gray" />
) : (
<TbDeviceMobile size={14} color="gray" />
)}
<Text size="xs" fw={500}>{bug.device} ({bug.os})</Text>
</Group>
</Box>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>SOURCE</Text>
<Badge variant="light" color="gray" size="sm">{bug.source}</Badge>
</Box>
</SimpleGrid>
{/* Feedback & Reporter Info */}
{(bug.user || bug.feedBack) && (
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
{bug.user && (
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>REPORTED BY</Text>
<Group gap="xs">
<Avatar src={bug.user.image} radius="xl" size="sm" color="blue">
{bug.user.name?.charAt(0).toUpperCase()}
</Avatar>
<Text size="sm">{bug.user.name}</Text>
</Group>
</Box>
)}
{bug.feedBack && (
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVELOPER FEEDBACK</Text>
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>{bug.feedBack}</Text>
</Box>
)}
</SimpleGrid>
)}
{/* Stack Trace */}
{bug.stackTrace && (
<Box>
<Group justify="space-between" mb={showStackTrace[bug.id] ? 8 : 0}>
<Text size="xs" fw={700} c="dimmed">STACK TRACE</Text>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => toggleStackTrace(bug.id)}
>
{showStackTrace[bug.id] ? 'Hide' : 'Show'}
</Button>
</Group>
<Collapse in={showStackTrace[bug.id]}>
<Code
block
color="red"
style={{
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
fontSize: '11px',
border: '1px solid var(--mantine-color-default-border)',
}}
>
{bug.stackTrace}
</Code>
</Collapse>
</Box>
)}
{/* Images */}
{bug.images && bug.images.length > 0 && (
<Box>
<Group gap="xs" mb={8}>
<TbPhoto size={16} color="gray" />
<Text size="xs" fw={700} c="dimmed">ATTACHED IMAGES ({bug.images.length})</Text>
</Group>
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
{bug.images.map((img: any) => (
<Paper
key={img.id}
withBorder
radius="md"
style={{ overflow: 'hidden', cursor: 'zoom-in' }}
onClick={() => setPreviewImage(img.imageUrl)}
>
<Image src={img.imageUrl} alt="Error screenshot" height={100} fit="cover" />
</Paper>
))}
</SimpleGrid>
</Box>
)}
{/* Logs / History */}
{bug.logs && bug.logs.length > 0 && (
<Box>
<Group justify="space-between" mb={showLogs[bug.id] ? 12 : 0}>
<Group gap="xs">
<TbHistory size={16} color="gray" />
<Text size="xs" fw={700} c="dimmed">ACTIVITY LOG ({bug.logs.length})</Text>
</Group>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => toggleLogs(bug.id)}
>
{showLogs[bug.id] ? 'Hide' : 'Show'}
</Button>
</Group>
<Collapse in={showLogs[bug.id]}>
<Timeline active={bug.logs.length - 1} bulletSize={24} lineWidth={2} mt="md">
{bug.logs.map((log: any) => (
<Timeline.Item
key={log.id}
bullet={
<Badge size="xs" circle color={log.status === 'RESOLVED' ? 'teal' : 'blue'}> </Badge>
}
title={<Text size="sm" fw={600}>{log.status}</Text>}
>
<Text size="xs" c="dimmed" mb={4}>
{new Date(log.createdAt).toLocaleString()} by {log.user?.name || 'Unknown'}
</Text>
<Text size="sm">{log.description}</Text>
</Timeline.Item>
))}
</Timeline>
</Collapse>
</Box>
)}
<Group justify="flex-end" pt="sm">
<Button variant="light" size="compact-xs" color="blue" onClick={() => {
setSelectedBugId(bug.id)
setFeedbackForm({ feedBack: bug.feedBack || '' })
openFeedbackModal()
}}>Developer Feedback</Button>
<Button variant="light" size="compact-xs" color="teal" onClick={() => {
setSelectedBugId(bug.id)
setUpdateForm({ status: bug.status, description: '' })
openUpdateModal()
}}>Update Status</Button>
</Group>
</Stack>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
)}
{totalPages > 1 && (
<Group justify="center" mt="xl">
<Pagination total={totalPages} value={page} onChange={setPage} radius="xl" />
</Group>
)}
</Paper>
</Stack>
</Container>
</DashboardLayout>
)
}

View File

@@ -1,23 +1,23 @@
import { useQuery } from '@tanstack/react-query' import { AppCard } from '@/frontend/components/AppCard'
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { StatsCard } from '@/frontend/components/StatsCard'
import { useSession } from '@/frontend/hooks/useAuth'
import { import {
Badge, Badge,
Button, Button,
Container, Container,
Group, Group,
Loader,
Paper,
SimpleGrid, SimpleGrid,
Stack, Stack,
Table,
Text, Text,
Title, Title,
Paper,
Table,
Loader,
} from '@mantine/core' } from '@mantine/core'
import { createFileRoute, redirect, Link } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query'
import { TbActivity, TbApps, TbMessageReport, TbUsers, TbChevronRight } from 'react-icons/tb' import { createFileRoute, Link, redirect } from '@tanstack/react-router'
import { useLogout, useSession } from '@/frontend/hooks/useAuth' import { TbApps, TbChevronRight, TbMessageReport, TbUsers } from 'react-icons/tb'
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { StatsCard } from '@/frontend/components/StatsCard'
import { AppCard } from '@/frontend/components/AppCard'
export const Route = createFileRoute('/dashboard')({ export const Route = createFileRoute('/dashboard')({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
@@ -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
@@ -36,12 +35,6 @@ export const Route = createFileRoute('/dashboard')({
component: DashboardPage, component: DashboardPage,
}) })
const recentErrors = [
{ id: 1, app: 'Desa+', message: 'NullPointerException at village_sync.dart:45', version: '2.4.1', time: '2 mins ago', severity: 'critical' },
{ id: 2, app: 'E-Commerce', message: 'Failed to load checkout session', version: '1.8.0', time: '15 mins ago', severity: 'high' },
{ id: 3, app: 'Fitness App', message: 'SocketException: Connection timed out', version: '0.9.5', time: '1 hour ago', severity: 'medium' },
]
function DashboardPage() { function DashboardPage() {
const { data: sessionData } = useSession() const { data: sessionData } = useSession()
const user = sessionData?.user const user = sessionData?.user
@@ -56,6 +49,20 @@ function DashboardPage() {
queryFn: () => fetch('/api/apps').then((r) => r.json()), queryFn: () => fetch('/api/apps').then((r) => r.json()),
}) })
const { data: recentErrors = [], isLoading: recentErrorsLoading } = useQuery({
queryKey: ['dashboard', 'recent-errors'],
queryFn: () => fetch('/api/dashboard/recent-errors').then((r) => r.json()),
})
const formatTimeAgo = (dateStr: string) => {
const diff = new Date().getTime() - new Date(dateStr).getTime()
const minutes = Math.floor(diff / 60000)
if (minutes < 60) return `${minutes || 1} mins ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours} hours ago`
return `${Math.floor(hours / 24)} days ago`
}
return ( return (
<DashboardLayout> <DashboardLayout>
<Container size="xl" py="lg"> <Container size="xl" py="lg">
@@ -65,7 +72,7 @@ function DashboardPage() {
<Title order={2} className="gradient-text">Overview Dashboard</Title> <Title order={2} className="gradient-text">Overview Dashboard</Title>
<Text size="sm" c="dimmed">Welcome back, {user?.name}. Here is what's happening today.</Text> <Text size="sm" c="dimmed">Welcome back, {user?.name}. Here is what's happening today.</Text>
</Stack> </Stack>
<Button {/* <Button
variant="gradient" variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }} gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbApps size={18} />} leftSection={<TbApps size={18} />}
@@ -74,7 +81,7 @@ function DashboardPage() {
to="/apps" to="/apps"
> >
Manage All Apps Manage All Apps
</Button> </Button> */}
</Group> </Group>
{statsLoading ? ( {statsLoading ? (
@@ -86,29 +93,29 @@ function DashboardPage() {
value={stats?.totalApps || 0} value={stats?.totalApps || 0}
icon={TbApps} icon={TbApps}
color="brand-blue" color="brand-blue"
trend={{ value: stats?.trends?.totalApps.toString() || '0', positive: true }} // trend={{ value: stats?.trends?.totalApps.toString() || '0', positive: true }}
/> />
<StatsCard <StatsCard
title="New Errors" title="New Errors"
value={stats?.newErrors || 0} value={stats?.newErrors || 0}
icon={TbMessageReport} icon={TbMessageReport}
color="brand-purple" color="brand-purple"
trend={{ value: stats?.trends?.newErrors.toString() || '0', positive: false }} // trend={{ value: stats?.trends?.newErrors.toString() || '0', positive: false }}
/> />
<StatsCard <StatsCard
title="Active Users" title="Users"
value={stats?.activeUsers || 0} value={stats?.activeUsers || 0}
icon={TbUsers} icon={TbUsers}
color="teal" color="teal"
trend={{ value: stats?.trends?.activeUsers.toString() || '0', positive: true }} // trend={{ value: stats?.trends?.activeUsers.toString() || '0', positive: true }}
/> />
</SimpleGrid> </SimpleGrid>
)} )}
<Group justify="space-between" mt="md"> <Group justify="space-between" mt="md">
<Title order={3}>Registered Applications</Title> <Title order={3}>Registered Applications</Title>
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />}> <Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />} component={Link} to="/apps">
View Report View All Apps
</Button> </Button>
</Group> </Group>
@@ -124,7 +131,7 @@ function DashboardPage() {
<Group justify="space-between" mt="md"> <Group justify="space-between" mt="md">
<Title order={3}>Recent Error Reports</Title> <Title order={3}>Recent Error Reports</Title>
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />}> <Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />} component={Link} to="/bug-reports">
View All Errors View All Errors
</Button> </Button>
</Group> </Group>
@@ -141,23 +148,35 @@ function DashboardPage() {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{recentErrors.map((error) => ( {recentErrorsLoading ? (
<Table.Tr>
<Table.Td colSpan={5} align="center" py="xl">
<Loader size="sm" type="dots" />
</Table.Td>
</Table.Tr>
) : recentErrors.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={5} align="center" py="xl">
<Text c="dimmed" size="sm">No recent errors found.</Text>
</Table.Td>
</Table.Tr>
) : recentErrors.map((error: any) => (
<Table.Tr key={error.id}> <Table.Tr key={error.id}>
<Table.Td> <Table.Td>
<Text fw={600} size="sm">{error.app}</Text> <Text fw={600} size="sm" style={{ textTransform: 'uppercase' }}>{error.app}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text size="sm" c="dimmed">{error.message}</Text> <Text size="sm" c="dimmed" lineClamp={1}>{error.message}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Badge variant="light" color="gray">v{error.version}</Badge> <Badge variant="light" color="gray">v{error.version}</Badge>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text size="xs" c="dimmed">{error.time}</Text> <Text size="xs" c="dimmed">{formatTimeAgo(error.time)}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Badge <Badge
color={error.severity === 'critical' ? 'red' : error.severity === 'high' ? 'orange' : 'yellow'} color={error.severity === 'OPEN' ? 'red' : error.severity === 'IN_PROGRESS' || error.severity === 'ON_HOLD' ? 'orange' : 'yellow'}
variant="dot" variant="dot"
> >
{error.severity.toUpperCase()} {error.severity.toUpperCase()}

1717
src/frontend/routes/dev.tsx Normal file

File diff suppressed because it is too large Load Diff

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,8 @@ 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' }) const dest = data.user.role === 'DEVELOPER' ? '/dev' : data.user.role === 'USER' ? '/profile' : '/dashboard'
throw redirect({ to: dest })
} }
} catch (e) { } catch (e) {
if (e instanceof Error) return if (e instanceof Error) return
@@ -57,15 +58,17 @@ 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_denied: 'Login dengan Google dibatalkan.',
invalid_state: 'Sesi OAuth tidak valid, silakan coba lagi.',
token_failed: 'Gagal menukar token Google, silakan coba lagi.',
userinfo_failed: 'Gagal mengambil info akun Google, silakan coba lagi.',
account_disabled: 'Akun Anda telah dinonaktifkan. Hubungi admin untuk informasi lebih lanjut.',
}[searchError ?? ''] ?? 'Login dengan Google gagal, silakan coba lagi.'
)}
</Alert> </Alert>
)} )}
@@ -99,13 +102,12 @@ function LoginPage() {
<Divider label="or" labelPosition="center" /> <Divider label="or" labelPosition="center" />
<Button <Button
component="a"
href="/api/auth/google"
fullWidth
variant="default" variant="default"
fullWidth
leftSection={<FcGoogle size={18} />} leftSection={<FcGoogle size={18} />}
onClick={() => { window.location.href = '/api/auth/google' }}
> >
Login with Google Continue with Google
</Button> </Button>
</Stack> </Stack>
</form> </form>

View File

@@ -1,213 +1,170 @@
import { import {
ActionIcon,
Badge, Badge,
Center,
Container, Container,
Group, Group,
Stack, Loader,
Text, Pagination,
Paper, SegmentedControl,
TextInput,
Select, Select,
Avatar, Stack,
Box, Table,
Divider, Text,
Title,
} from '@mantine/core' } from '@mantine/core'
import { useState, useMemo } from 'react' import { DatePickerInput, type DatesRangeValue } from '@mantine/dates'
import dayjs from 'dayjs'
import 'dayjs/locale/id'
import { useMemo, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { TbSearch, TbClock, TbCheck, TbX } from 'react-icons/tb' import { TbRefresh } from 'react-icons/tb'
import { DashboardLayout } from '@/frontend/components/DashboardLayout' import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import useSWR from 'swr'
import { API_URLS } from '../config/api'
export const Route = createFileRoute('/logs')({ export const Route = createFileRoute('/logs')({
component: GlobalLogsPage, component: GlobalLogsPage,
}) })
const timelineData = [ const fetcher = (url: string) => fetch(url, { credentials: 'include' }).then((r) => r.json())
{
date: 'TODAY', const LOG_TYPES = ['all', 'LOGIN', 'LOGOUT', 'CREATE', 'UPDATE', 'DELETE'] as const
logs: [ const LOG_TYPE_COLOR: Record<string, string> = {
{ id: 1, time: '12:12 PM', operator: 'Budi Santoso', app: 'Desa+', color: 'blue', content: <>generated document <Badge variant="light" color="gray" radius="sm">Surat Domisili</Badge> for <Badge variant="light" color="blue" radius="sm">Sukatani</Badge></> }, LOGIN: 'green',
{ id: 2, time: '11:42 AM', operator: 'Siti Aminah', app: 'Desa+', color: 'teal', content: <>uploaded financial report <Badge variant="light" color="gray" radius="sm">Realisasi Q1</Badge> for <Badge variant="light" color="teal" radius="sm">Sukamaju</Badge></> }, LOGOUT: 'gray',
{ id: 3, time: '10:12 AM', operator: 'System', app: 'Desa+', color: 'red', icon: TbX, content: <>experienced failure in <Badge variant="light" color="violet" radius="sm">SIAK Sync</Badge> at <Badge variant="light" color="red" radius="sm" leftSection={<TbX size={12}/>}>Cikini</Badge></>, message: { title: 'Sync Operation Failed (NullPointerException)', text: 'NullPointerException at village_sync.dart:45. The server returned a timeout error while waiting for the master database replica connection. Auto-retry scheduled in 15 minutes.' } }, CREATE: 'blue',
{ id: 4, time: '09:42 AM', operator: 'Jane Smith', app: 'E-Commerce', color: 'orange', icon: TbCheck, content: <>resolved payment gateway issue for <Badge variant="light" color="orange" radius="sm">E-Commerce</Badge> checkout</> }, UPDATE: 'yellow',
] DELETE: 'red',
}, }
{
date: 'YESTERDAY',
logs: [
{ id: 5, time: '05:10 AM', operator: 'System', app: 'System', color: 'cyan', content: <>completed automated <Badge variant="light" color="cyan" radius="sm">Nightly Backup</Badge> for all 138 villages</> },
{ id: 6, time: '04:50 AM', operator: 'Rahmat Hidayat', app: 'Desa+', color: 'green', content: <>granted Admin access to <Text component="span" fw={600}>Desa Bojong Gede</Text> operator</> },
{ id: 7, time: '03:42 AM', operator: 'System', app: 'Fitness App', color: 'red', icon: TbX, content: <>detected SocketException across <Badge variant="light" color="violet" radius="sm">Fitness App</Badge> wearable sync operations.</> },
{ id: 8, time: '02:33 AM', operator: 'Agus Setiawan', app: 'Desa+', color: 'blue', content: <>verified 145 <Badge variant="light" color="gray" radius="sm">Surat Kematian</Badge> entries in batch.</> },
]
},
{
date: '12 APRIL, 2026',
logs: [
{ id: 9, time: '03:42 AM', operator: 'Amel', app: 'Desa+', color: 'indigo', content: <>changed version configurations rolling out <Badge variant="light" color="gray" radius="sm">Desa+ v2.4.1</Badge></> },
{ id: 10, time: '02:10 AM', operator: 'John Doe', app: 'E-Commerce', color: 'pink', content: <>updated App setting <Badge variant="light" color="gray" radius="sm">Require OTP on Login</Badge> <Text component="span" c="violet" fw={600} size="sm" style={{ cursor: 'pointer' }}>View Details</Text></> },
]
}
]
function GlobalLogsPage() { function GlobalLogsPage() {
const [search, setSearch] = useState('') const [type, setType] = useState('all')
const [appFilter, setAppFilter] = useState<string | null>(null) const [operatorId, setOperatorId] = useState('all')
const [operatorFilter, setOperatorFilter] = useState<string | null>(null) const [dateRange, setDateRange] = useState<DatesRangeValue>([null, null])
const [page, setPage] = useState(1)
const filteredTimeline = useMemo(() => { const { data: operatorsData } = useSWR(API_URLS.getLogOperators(), fetcher)
return timelineData
.map(group => { const operatorOptions = useMemo(() => {
const filteredLogs = group.logs.filter(log => { if (!operatorsData || !Array.isArray(operatorsData)) return [{ value: 'all', label: 'Semua operator' }]
if (appFilter && log.app !== appFilter) return false; return [
if (operatorFilter && log.operator !== operatorFilter) return false; { value: 'all', label: 'Semua user' },
if (search) { ...operatorsData.map((op: any) => ({ value: op.id, label: op.name })),
const lSearch = search.toLowerCase(); ]
if (!log.operator.toLowerCase().includes(lSearch) && !log.app.toLowerCase().includes(lSearch)) { }, [operatorsData])
return false;
} const dateFrom = dateRange[0] ? dayjs(dateRange[0]).format('YYYY-MM-DD') : undefined
} const dateTo = dateRange[1] ? dayjs(dateRange[1]).format('YYYY-MM-DD') : undefined
return true;
}); const { data, isLoading, mutate } = useSWR(
return { ...group, logs: filteredLogs }; API_URLS.getGlobalLogs(page, '', type, operatorId, dateFrom, dateTo),
}) fetcher,
.filter(group => group.logs.length > 0); { refreshInterval: 10_000 },
}, [search, appFilter, operatorFilter]); )
const logs: any[] = data?.data ?? []
const totalPages: number = data?.totalPages ?? 1
return ( return (
<DashboardLayout> <DashboardLayout>
<Container size="xl" py="lg"> <Container size="xl" py="lg">
<Stack>
{/* Header Controls */} <Group justify="space-between">
<Group mb="xl" gap="md"> <Title order={3}>Activity Logs</Title>
<TextInput <ActionIcon variant="subtle" color="gray" onClick={() => mutate()}>
placeholder="Search operator or app..." <TbRefresh size={16} />
leftSection={<TbSearch size={16} />} </ActionIcon>
radius="md" </Group>
w={220}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<Select
placeholder="All Applications"
data={['Desa+', 'E-Commerce', 'Fitness App', 'System']}
radius="md"
w={160}
clearable
value={appFilter}
onChange={setAppFilter}
/>
<Select
placeholder="All Operators"
data={['Agus Setiawan', 'Amel', 'Budi Santoso', 'Jane Smith', 'John Doe', 'Rahmat Hidayat', 'Siti Aminah', 'System']}
radius="md"
w={160}
clearable
value={operatorFilter}
onChange={setOperatorFilter}
/>
</Group>
{/* Timeline Content */} <Group gap="sm" wrap="wrap">
<Paper withBorder p="xl" radius="2xl" className="glass" style={{ background: 'var(--mantine-color-body)' }}> <Select
{filteredTimeline.length === 0 ? ( placeholder="Filter user"
<Text c="dimmed" ta="center" py="xl">No logs found matching your filters.</Text> value={operatorId}
) : filteredTimeline.map((group, groupIndex) => ( onChange={(v) => { setOperatorId(v ?? 'all'); setPage(1) }}
<Box key={group.date}> data={operatorOptions}
<Text w={180}
size="xs" clearable
fw={700} />
c="dimmed" <DatePickerInput
mt={groupIndex > 0 ? "xl" : 0} type="range"
mb="lg" placeholder="Filter tanggal"
style={{ textTransform: 'uppercase' }} value={dateRange}
> onChange={(v) => { setDateRange(v); setPage(1) }}
{group.date} locale="id"
</Text> valueFormat="DD MMM YYYY"
clearable
<Stack gap={0} pl={4}> w={300}
{group.logs.map((log, logIndex) => { />
const isLastLog = logIndex === group.logs.length - 1; <SegmentedControl
value={type}
return ( onChange={(v) => { setType(v); setPage(1) }}
<Group data={LOG_TYPES.map((t) => ({ label: t === 'all' ? 'All' : t, value: t }))}
key={log.id} />
wrap="nowrap" </Group>
align="flex-start"
gap="lg"
style={{ position: 'relative', paddingBottom: isLastLog ? 0 : 32 }}
>
{/* Left: Time */}
<Text
size="xs"
c="dimmed"
w={70}
style={{ flexShrink: 0, marginTop: 4, textAlign: 'left' }}
>
{log.time}
</Text>
{/* Middle: Line & Avatar */} {isLoading ? (
<Box style={{ position: 'relative', width: 20, flexShrink: 0, alignSelf: 'stretch' }}> <Center py="xl"><Loader /></Center>
{/* Vertical Line */} ) : (
{!isLastLog && ( <>
<Box <Table.ScrollContainer minWidth={600}>
style={{ <Table striped highlightOnHover fz="xs" style={{ tableLayout: 'fixed', width: '100%' }}>
position: 'absolute', <colgroup>
top: 24, <col style={{ width: 160 }} />
bottom: -8, <col style={{ width: 200 }} />
left: '50%', <col style={{ width: 100 }} />
transform: 'translateX(-50%)', <col />
width: 1, </colgroup>
backgroundColor: 'rgba(128,128,128,0.2)' <Table.Thead>
}} <Table.Tr>
/> <Table.Th>Time</Table.Th>
)} <Table.Th>Operator</Table.Th>
{/* Avatar */} <Table.Th>Type</Table.Th>
<Box style={{ position: 'relative', zIndex: 2 }}> <Table.Th>Message</Table.Th>
{log.icon ? ( </Table.Tr>
<Avatar size={24} radius="xl" color={log.color} variant="light"> </Table.Thead>
<log.icon size={14} /> <Table.Tbody>
</Avatar> {logs.map((log: any) => (
) : ( <Table.Tr key={log.id}>
<Avatar size={24} radius="xl" color={log.color}> <Table.Td style={{ whiteSpace: 'nowrap' }}>
{log.operator.charAt(0)} {new Date(log.createdAt).toLocaleString('id-ID')}
</Avatar> </Table.Td>
)} <Table.Td>
</Box> {log.user ? (
</Box> <div>
<Text fw={500} truncate>{log.user.name}</Text>
{/* Right: Content */} <Text c="dimmed" truncate>{log.user.email}</Text>
<Box style={{ flexGrow: 1, marginTop: 2 }}> </div>
<Text size="sm"> ) : <Text c="dimmed"></Text>}
<Text component="span" fw={600} mr={4}>{log.operator}</Text> </Table.Td>
{log.content} <Table.Td>
</Text> <Badge color={LOG_TYPE_COLOR[log.type] ?? 'gray'} variant="light">
{log.type}
{log.message && ( </Badge>
<Paper </Table.Td>
withBorder <Table.Td>
p="md" <Text>{log.message}</Text>
radius="md" </Table.Td>
mt="sm" </Table.Tr>
style={{ maxWidth: 800, backgroundColor: 'transparent' }} ))}
> {logs.length === 0 && (
<Text size="sm" fw={600} mb={4}>{log.message.title}</Text> <Table.Tr>
<Text size="sm" c="dimmed"> <Table.Td colSpan={4}>
{log.message.text} <Center py="xl"><Text c="dimmed">Belum ada log aktivitas</Text></Center>
</Text> </Table.Td>
</Paper> </Table.Tr>
)} )}
</Box> </Table.Tbody>
</Group> </Table>
) </Table.ScrollContainer>
})} {totalPages > 1 && (
</Stack> <Center>
<Pagination total={totalPages} value={page} onChange={setPage} size="sm" />
{groupIndex < timelineData.length - 1 && ( </Center>
<Divider my="xl" color="rgba(128,128,128,0.1)" />
)} )}
</Box> </>
))} )}
</Paper> </Stack>
</Container> </Container>
</DashboardLayout> </DashboardLayout>
) )

View File

@@ -1,4 +1,5 @@
import { import {
Alert,
Avatar, Avatar,
Badge, Badge,
Button, Button,
@@ -10,7 +11,7 @@ import {
Title, Title,
} from '@mantine/core' } from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { TbLogout, TbUser } from 'react-icons/tb' import { TbClock, TbLogout, TbUser } from 'react-icons/tb'
import { useLogout, useSession } from '@/frontend/hooks/useAuth' import { useLogout, useSession } from '@/frontend/hooks/useAuth'
export const Route = createFileRoute('/profile')({ export const Route = createFileRoute('/profile')({
@@ -30,9 +31,9 @@ export const Route = createFileRoute('/profile')({
}) })
const roleBadgeColor: Record<string, string> = { const roleBadgeColor: Record<string, string> = {
USER: 'blue', USER: 'gray',
ADMIN: 'violet', ADMIN: 'violet',
SUPER_ADMIN: 'red', DEVELOPER: 'red',
} }
function ProfilePage() { function ProfilePage() {
@@ -56,9 +57,26 @@ function ProfilePage() {
</Button> </Button>
</Group> </Group>
{user?.role === 'USER' && (
<Alert
icon={<TbClock size={18} />}
title="Akun Menunggu Persetujuan"
color="yellow"
variant="light"
radius="md"
>
Akun kamu sedang menunggu persetujuan admin. Hubungi admin atau developer untuk mendapatkan akses ke fitur dashboard.
</Alert>
)}
<Paper withBorder p="xl" radius="md"> <Paper withBorder p="xl" radius="md">
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Avatar color="blue" radius="xl" size={80}> <Avatar
src={user?.image ?? undefined}
color="blue"
radius="xl"
size={80}
>
{user?.name?.charAt(0).toUpperCase()} {user?.name?.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>

View File

@@ -1,73 +1,259 @@
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { StatsCard } from '@/frontend/components/StatsCard'
import { import {
ActionIcon, ActionIcon,
Avatar,
Badge, Badge,
Box,
Button, Button,
Card, Card,
Container, Container,
Divider,
Group, Group,
List,
Modal,
Pagination,
Paper,
PasswordInput,
Select,
SimpleGrid,
Stack, Stack,
Table, Table,
Tabs,
Text, Text,
TextInput, TextInput,
Title,
Paper,
Tabs,
Avatar,
SimpleGrid,
ThemeIcon, ThemeIcon,
List, Title,
Box, Tooltip,
Divider,
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { import { useEffect, useState } from 'react'
TbPlus, import {
TbSearch,
TbPencil,
TbTrash,
TbUserCheck,
TbShieldCheck,
TbAccessPoint, TbAccessPoint,
TbCircleCheck, TbCircleCheck,
TbClock, TbCircleX,
TbApps, TbPencil,
TbPlus,
TbSearch,
TbShieldCheck,
TbTrash,
TbUserCheck,
TbUserPlus,
} from 'react-icons/tb' } from 'react-icons/tb'
import { DashboardLayout } from '@/frontend/components/DashboardLayout' import useSWR from 'swr'
import { StatsCard } from '@/frontend/components/StatsCard' import { API_URLS } from '../config/api'
import { useSession } from '../hooks/useAuth'
export const Route = createFileRoute('/users')({ export const Route = createFileRoute('/users')({
component: UsersPage, component: UsersPage,
}) })
const mockUsers = [ const fetcher = (url: string) => fetch(url).then((res) => res.json())
{ id: 1, name: 'Amel', email: 'amel@company.com', role: 'SUPER_ADMIN', apps: 'All', status: 'Online', lastActive: 'Now' },
{ id: 2, name: 'John Doe', email: 'john@company.com', role: 'DEVELOPER', apps: 'Desa+, Fitness App', status: 'Offline', lastActive: '2h ago' }, const getRoleColor = (role: string) => {
{ id: 3, name: 'Jane Smith', email: 'jane@company.com', role: 'QA', apps: 'E-Commerce', status: 'Online', lastActive: '12m ago' }, if (role === 'DEVELOPER') return 'violet'
{ id: 4, name: 'Rahmat Hidayat', email: 'rahmat@company.com', role: 'DEVELOPER', apps: 'Desa+', status: 'Online', lastActive: 'Now' }, if (role === 'ADMIN') return 'brand-blue'
] return 'gray'
}
const roles = [ const roles = [
{ {
name: 'SUPER_ADMIN', name: 'DEVELOPER',
count: 2, color: 'violet',
color: 'red', description: 'Super admin dengan akses penuh ke seluruh sistem termasuk Dev Console.',
permissions: ['Full Access', 'User Mgmt', 'Role Mgmt', 'App Config', 'Logs & Errors'] permissions: [
'Akses Dev Console (/dev)',
'Manajemen user & role',
'Kelola bug report & feedback',
'Lihat semua app & log aktivitas',
'Kelola versi & status aplikasi',
'Hapus log sistem',
],
}, },
{ {
name: 'DEVELOPER', name: 'ADMIN',
count: 12, color: 'blue',
color: 'brand-blue', description: 'Operator yang dapat mengelola aplikasi, bug, dan melihat log aktivitas.',
permissions: ['View All Apps', 'Manage Assigned App', 'View Logs', 'Resolve Errors', 'Village Setup'] permissions: [
'Lihat & kelola semua aplikasi',
'Kelola bug report',
'Lihat log aktivitas',
'Lihat data user, desa, orders',
'Update status village & produk',
],
}, },
{ {
name: 'QA', name: 'USER',
count: 5, color: 'gray',
color: 'orange', description: 'Akun baru yang belum disetujui. Menunggu approval dari Admin atau Developer.',
permissions: ['View All Apps', 'View Logs', 'Report Errors', 'Test App Features'] permissions: [
'Akses halaman profil',
'Lihat status persetujuan akun',
],
}, },
] ]
function UsersPage() { function UsersPage() {
const [search, setSearch] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [page, setPage] = useState(1)
const { data: session } = useSession()
const isDeveloper = session?.user?.role === 'DEVELOPER'
useEffect(() => {
const timer = setTimeout(() => setDebouncedSearch(search), 300)
return () => clearTimeout(timer)
}, [search])
const { data: stats, mutate: mutateStats } = useSWR(API_URLS.getOperatorStats(), fetcher)
const { data: response, isLoading, mutate: mutateOperators } = useSWR(
API_URLS.getOperators(page, debouncedSearch),
fetcher
)
const operators = response?.data || []
// ── Create User Modal ──
const [createOpened, { open: openCreate, close: closeCreate }] = useDisclosure(false)
const [isCreating, setIsCreating] = useState(false)
const [createForm, setCreateForm] = useState({
name: '',
email: '',
password: '',
role: 'ADMIN',
})
const handleCreateUser = async () => {
if (!createForm.name || !createForm.email || !createForm.password) {
notifications.show({ title: 'Validation Error', message: 'Please fill in all required fields.', color: 'red' })
return
}
setIsCreating(true)
try {
const res = await fetch(API_URLS.createOperator(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(createForm),
})
if (res.ok) {
notifications.show({ title: 'Success', message: 'User has been created.', color: 'teal', icon: <TbCircleCheck size={18} /> })
mutateOperators()
mutateStats()
closeCreate()
setCreateForm({ name: '', email: '', password: '', role: 'ADMIN' })
} else {
const err = await res.json()
throw new Error(err.error || 'Failed to create user')
}
} catch (e: any) {
notifications.show({ title: 'Error', message: e.message || 'Something went wrong.', color: 'red', icon: <TbCircleX size={18} /> })
} finally {
setIsCreating(false)
}
}
// ── Edit User Modal ──
const [editOpened, { open: openEdit, close: closeEdit }] = useDisclosure(false)
const [isEditing, setIsEditing] = useState(false)
const [editingUserId, setEditingUserId] = useState<string | null>(null)
const [editForm, setEditForm] = useState({
name: '',
email: '',
role: '',
})
const handleOpenEdit = (user: any) => {
setEditingUserId(user.id)
setEditForm({ name: user.name, email: user.email, role: user.role })
openEdit()
}
const handleEditUser = async () => {
if (!editingUserId || !editForm.name || !editForm.email) return
setIsEditing(true)
try {
const res = await fetch(API_URLS.editOperator(editingUserId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editForm),
})
if (res.ok) {
notifications.show({ title: 'Success', message: 'User has been updated.', color: 'teal', icon: <TbCircleCheck size={18} /> })
mutateOperators()
closeEdit()
} else {
throw new Error('Failed to update user')
}
} catch (e) {
notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: <TbCircleX size={18} /> })
} finally {
setIsEditing(false)
}
}
// ── Delete User ──
const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false)
const [isDeleting, setIsDeleting] = useState(false)
const [deletingUser, setDeletingUser] = useState<any>(null)
const handleOpenDelete = (user: any) => {
setDeletingUser(user)
openDelete()
}
const handleDeleteUser = async () => {
if (!deletingUser) return
setIsDeleting(true)
try {
const res = await fetch(API_URLS.deleteOperator(deletingUser.id), {
method: 'DELETE',
})
if (res.ok) {
notifications.show({ title: 'Success', message: 'User has been deleted.', color: 'teal', icon: <TbCircleCheck size={18} /> })
mutateOperators()
mutateStats()
closeDelete()
} else {
const err = await res.json()
throw new Error(err.error || 'Failed to delete user')
}
} catch (e: any) {
notifications.show({ title: 'Error', message: e.message || 'Something went wrong.', color: 'red', icon: <TbCircleX size={18} /> })
} finally {
setIsDeleting(false)
}
}
// ── Activate User ──
const handleActivateUser = async (user: any) => {
try {
const res = await fetch(`/api/operators/${user.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ active: true }),
})
if (res.ok) {
notifications.show({ title: 'Success', message: `${user.name} telah diaktifkan kembali.`, color: 'teal', icon: <TbCircleCheck size={18} /> })
mutateOperators()
mutateStats()
} else {
const err = await res.json()
throw new Error(err.error || 'Failed to activate user')
}
} catch (e: any) {
notifications.show({ title: 'Error', message: e.message || 'Something went wrong.', color: 'red', icon: <TbCircleX size={18} /> })
}
}
return ( return (
<DashboardLayout> <DashboardLayout>
<Container size="xl" py="lg"> <Container size="xl" py="lg">
@@ -80,9 +266,9 @@ function UsersPage() {
</Group> </Group>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="lg"> <SimpleGrid cols={{ base: 1, sm: 3 }} spacing="lg">
<StatsCard title="Total Staff" value={24} icon={TbUserCheck} color="brand-blue" /> <StatsCard title="Total Staff" value={stats?.totalStaff ?? 0} icon={TbUserCheck} color="brand-blue" />
<StatsCard title="Active Now" value={18} icon={TbAccessPoint} color="teal" /> <StatsCard title="Active Now" value={stats?.activeNow ?? 0} icon={TbAccessPoint} color="teal" />
<StatsCard title="Security Roles" value={3} icon={TbShieldCheck} color="purple-primary" /> <StatsCard title="Security Roles" value={stats?.rolesCount ?? 0} icon={TbShieldCheck} color="purple-primary" />
</SimpleGrid> </SimpleGrid>
<Tabs defaultValue="users" color="brand-blue" variant="pills" radius="md"> <Tabs defaultValue="users" color="brand-blue" variant="pills" radius="md">
@@ -100,15 +286,23 @@ function UsersPage() {
radius="md" radius="md"
w={350} w={350}
variant="filled" variant="filled"
value={search}
onChange={(e) => {
setSearch(e.currentTarget.value)
setPage(1)
}}
/> />
<Button {isDeveloper && (
variant="gradient" <Button
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }} variant="gradient"
leftSection={<TbPlus size={18} />} gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
radius="md" leftSection={<TbPlus size={18} />}
> radius="md"
Add New User onClick={openCreate}
</Button> >
Add New User
</Button>
)}
</Group> </Group>
<Paper withBorder radius="2xl" className="glass" p={0} style={{ overflow: 'hidden' }}> <Paper withBorder radius="2xl" className="glass" p={0} style={{ overflow: 'hidden' }}>
@@ -117,56 +311,101 @@ function UsersPage() {
<Table.Tr> <Table.Tr>
<Table.Th>Name & Contact</Table.Th> <Table.Th>Name & Contact</Table.Th>
<Table.Th>Role</Table.Th> <Table.Th>Role</Table.Th>
<Table.Th>Status</Table.Th> <Table.Th>Joined Date</Table.Th>
<Table.Th>App Access</Table.Th>
<Table.Th>Actions</Table.Th> <Table.Th>Actions</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{mockUsers.map((user) => ( {isLoading ? (
<Table.Tr key={user.id}> <Table.Tr>
<Table.Td> <Table.Td colSpan={4} align="center">
<Group gap="sm"> <Text size="sm" c="dimmed" py="xl">Loading user data...</Text>
<Avatar size="sm" radius="xl" color="brand-blue">{user.name.charAt(0)}</Avatar>
<Stack gap={0}>
<Text fw={600} size="sm">{user.name}</Text>
<Text size="xs" c="dimmed">{user.email}</Text>
</Stack>
</Group>
</Table.Td>
<Table.Td>
<Badge variant="light" color={user.role === 'SUPER_ADMIN' ? 'red' : user.role === 'DEVELOPER' ? 'brand-blue' : 'orange'}>
{user.role}
</Badge>
</Table.Td>
<Table.Td>
<Group gap={6}>
<Box style={{ width: 6, height: 6, borderRadius: '50%', background: user.status === 'Online' ? '#10b981' : '#94a3b8' }} />
<Text size="xs" fw={500}>{user.status}</Text>
<Text size="xs" c="dimmed" ml="xs"><TbClock size={10} style={{ marginBottom: -2 }} /> {user.lastActive}</Text>
</Group>
</Table.Td>
<Table.Td>
<Group gap={4}>
<TbApps size={12} color="gray" />
<Text size="xs" fw={500}>{user.apps}</Text>
</Group>
</Table.Td>
<Table.Td>
<Group gap="xs">
<ActionIcon variant="light" size="sm" color="blue">
<TbPencil size={14} />
</ActionIcon>
<ActionIcon variant="light" size="sm" color="red">
<TbTrash size={14} />
</ActionIcon>
</Group>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
))} ) : operators.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={4} align="center">
<Text size="sm" c="dimmed" py="xl">No users found.</Text>
</Table.Td>
</Table.Tr>
) : (
operators.map((user: any) => (
<Table.Tr key={user.id}>
<Table.Td style={{ opacity: user.active === false ? 0.45 : 1 }}>
<Group gap="sm">
<Box style={{ position: 'relative' }}>
<Avatar size="sm" radius="xl" color={user.active === false ? 'gray' : getRoleColor(user.role)} src={user.image}>
{user.name.charAt(0)}
</Avatar>
{user.active === false && (
<Box
style={{
position: 'absolute', bottom: -2, right: -2,
width: 10, height: 10, borderRadius: '50%',
background: 'var(--mantine-color-red-6)',
border: '1.5px solid var(--mantine-color-body)',
}}
/>
)}
</Box>
<Stack gap={0}>
<Group gap={6}>
<Text fw={600} size="sm" c={user.active === false ? 'dimmed' : undefined}>{user.name}</Text>
{user.active === false && (
<Badge size="xs" color="red" variant="light">Inactive</Badge>
)}
</Group>
<Text size="xs" c="dimmed">{user.email}</Text>
</Stack>
</Group>
</Table.Td>
<Table.Td style={{ opacity: user.active === false ? 0.45 : 1 }}>
<Badge variant="light" color={user.active === false ? 'gray' : getRoleColor(user.role)}>
{user.role}
</Badge>
</Table.Td>
<Table.Td style={{ opacity: user.active === false ? 0.45 : 1 }}>
<Text size="xs" fw={500} c={user.active === false ? 'dimmed' : undefined}>
{new Date(user.createdAt).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
{user.active === false ? (
<Tooltip label="Aktifkan user" withArrow>
<ActionIcon disabled={!isDeveloper} variant="light" size="sm" color="teal" onClick={() => handleActivateUser(user)}>
<TbUserPlus size={14} />
</ActionIcon>
</Tooltip>
) : (
<>
<ActionIcon disabled={!isDeveloper} variant="light" size="sm" color="blue" onClick={() => handleOpenEdit(user)}>
<TbPencil size={14} />
</ActionIcon>
<ActionIcon disabled={!isDeveloper} variant="light" size="sm" color="red" onClick={() => handleOpenDelete(user)}>
<TbTrash size={14} />
</ActionIcon>
</>
)}
</Group>
</Table.Td>
</Table.Tr>
))
)}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Paper> </Paper>
{response?.totalPages > 1 && (
<Group justify="center" mt="md">
<Pagination
total={response.totalPages}
value={page}
onChange={setPage}
radius="md"
/>
</Group>
)}
</Stack> </Stack>
</Tabs.Panel> </Tabs.Panel>
@@ -179,16 +418,15 @@ function UsersPage() {
<ThemeIcon size="xl" radius="md" color={role.color} variant="light"> <ThemeIcon size="xl" radius="md" color={role.color} variant="light">
<TbShieldCheck size={28} /> <TbShieldCheck size={28} />
</ThemeIcon> </ThemeIcon>
<Badge variant="default" size="lg" radius="sm">{role.count} Users</Badge>
</Group> </Group>
<Stack gap={4}> <Stack gap={4}>
<Title order={4}>{role.name.replace('_', ' ')}</Title> <Title order={4}>{role.name}</Title>
<Text size="sm" c="dimmed">Core role for secure app management.</Text> <Text size="sm" c="dimmed">{role.description}</Text>
</Stack> </Stack>
<Divider /> <Divider />
<Text size="xs" fw={700} c="dimmed" style={{ textTransform: 'uppercase' }}>Key Permissions</Text> <Text size="xs" fw={700} c="dimmed" style={{ textTransform: 'uppercase' }}>Key Permissions</Text>
<List <List
spacing="xs" spacing="xs"
@@ -205,9 +443,9 @@ function UsersPage() {
))} ))}
</List> </List>
<Button fullWidth variant="light" color={role.color} mt="md" radius="md"> {/* <Button fullWidth variant="light" color={role.color} mt="md" radius="md">
Edit Permissions Edit Permissions
</Button> </Button> */}
</Stack> </Stack>
</Card> </Card>
))} ))}
@@ -216,7 +454,128 @@ function UsersPage() {
</Tabs> </Tabs>
</Stack> </Stack>
</Container> </Container>
{/* Create User Modal */}
<Modal
opened={createOpened}
onClose={closeCreate}
title={<Text fw={700} size="lg">Add New User</Text>}
radius="xl"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<TextInput
label="Full Name"
placeholder="Enter full name"
required
value={createForm.name}
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
/>
<TextInput
label="Email"
placeholder="Enter email address"
required
value={createForm.email}
onChange={(e) => setCreateForm({ ...createForm, email: e.target.value })}
/>
<PasswordInput
label="Password"
placeholder="Enter password"
required
value={createForm.password}
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
/>
<Select
label="Role"
data={[
{ value: 'ADMIN', label: 'Admin' },
{ value: 'DEVELOPER', label: 'Developer' },
]}
value={createForm.role}
onChange={(val) => setCreateForm({ ...createForm, role: val || 'USER' })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isCreating}
onClick={handleCreateUser}
>
Create User
</Button>
</Stack>
</Modal>
{/* Edit User Modal */}
<Modal
opened={editOpened}
onClose={closeEdit}
title={<Text fw={700} size="lg">Edit User</Text>}
radius="xl"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<TextInput
label="Full Name"
placeholder="Enter full name"
required
value={editForm.name}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
/>
<TextInput
label="Email"
placeholder="Enter email address"
required
value={editForm.email}
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
/>
<Select
label="Role"
data={[
{ value: 'USER', label: 'User (Pending)' },
{ value: 'ADMIN', label: 'Admin' },
{ value: 'DEVELOPER', label: 'Developer' },
]}
value={editForm.role}
onChange={(val) => setEditForm({ ...editForm, role: val || 'ADMIN' })}
/>
<Button
fullWidth
mt="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isEditing}
onClick={handleEditUser}
>
Save Changes
</Button>
</Stack>
</Modal>
{/* Delete Confirmation Modal */}
<Modal
opened={deleteOpened}
onClose={closeDelete}
title={<Text fw={700} size="lg">Delete User</Text>}
radius="xl"
size="sm"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Text size="sm">
Are you sure you want to delete <Text component="span" fw={700}>{deletingUser?.name}</Text>? This action cannot be undone.
</Text>
<Group justify="flex-end" mt="md">
<Button variant="subtle" color="gray" onClick={closeDelete}>
Cancel
</Button>
<Button color="red" loading={isDeleting} onClick={handleDeleteUser}>
Delete User
</Button>
</Group>
</Stack>
</Modal>
</DashboardLayout> </DashboardLayout>
) )
} }

View File

@@ -84,7 +84,7 @@ body {
transition: var(--transition-smooth); transition: var(--transition-smooth);
} }
.sidebar-nav-item.active { .sidebar-nav-item[data-active] {
background: var(--gradient-blue-purple); background: var(--gradient-blue-purple);
color: white; color: white;
} }

View File

@@ -7,7 +7,7 @@ import { env } from './lib/env'
const isProduction = env.NODE_ENV === 'production' const isProduction = env.NODE_ENV === 'production'
// ─── Route Classification ────────────────────────────── // ─── Route Classification ──────────────────────────────
const API_PREFIXES = ['/api/', '/webhook/', '/ws/', '/health'] const API_PREFIXES = ['/api/', '/webhook/', '/ws/', '/health', '/docs']
function isApiRoute(pathname: string): boolean { function isApiRoute(pathname: string): boolean {
return API_PREFIXES.some((p) => pathname.startsWith(p)) || pathname === '/health' return API_PREFIXES.some((p) => pathname.startsWith(p)) || pathname === '/health'

45
src/lib/applog.ts Normal file
View File

@@ -0,0 +1,45 @@
import { redis } from './redis'
export type LogLevel = 'info' | 'warn' | 'error'
export interface AppLogEntry {
id: number
level: LogLevel
message: string
detail?: string
timestamp: string
}
const REDIS_KEY = 'app:logs'
const MAX_ENTRIES = 500
const ID_KEY = 'app:logs:next_id'
export async function appLog(level: LogLevel, message: string, detail?: string) {
if (!redis) return
const id = await redis.incr(ID_KEY)
const entry: AppLogEntry = { id, level, message, detail, timestamp: new Date().toISOString() }
await redis.lpush(REDIS_KEY, JSON.stringify(entry))
await redis.ltrim(REDIS_KEY, 0, MAX_ENTRIES - 1)
}
export async function getAppLogs(options?: {
level?: LogLevel
limit?: number
afterId?: number
}): Promise<AppLogEntry[]> {
if (!redis) return []
const limit = options?.limit ?? 100
const fetchCount = options?.level || options?.afterId ? MAX_ENTRIES : limit
const raw = await redis.lrange(REDIS_KEY, 0, fetchCount - 1)
let logs: AppLogEntry[] = raw.map((s: string) => JSON.parse(s))
if (options?.afterId) logs = logs.filter((l) => l.id > options.afterId!)
if (options?.level) logs = logs.filter((l) => l.level === options.level)
logs.reverse()
return logs.slice(-limit)
}
export async function clearAppLogs() {
if (!redis) return
await redis.del(REDIS_KEY)
await redis.del(ID_KEY)
}

View File

@@ -12,8 +12,17 @@ export const env = {
PORT: parseInt(optional('PORT', '3000'), 10), PORT: parseInt(optional('PORT', '3000'), 10),
NODE_ENV: optional('NODE_ENV', 'development'), NODE_ENV: optional('NODE_ENV', 'development'),
REACT_EDITOR: optional('REACT_EDITOR', 'code'), REACT_EDITOR: optional('REACT_EDITOR', 'code'),
BASE_URL: optional('BUN_PUBLIC_BASE_URL', 'http://localhost:3000'),
DATABASE_URL: required('DATABASE_URL'), DATABASE_URL: required('DATABASE_URL'),
GOOGLE_CLIENT_ID: required('GOOGLE_CLIENT_ID'), GOOGLE_CLIENT_ID: required('GOOGLE_CLIENT_ID'),
GOOGLE_CLIENT_SECRET: required('GOOGLE_CLIENT_SECRET'), GOOGLE_CLIENT_SECRET: required('GOOGLE_CLIENT_SECRET'),
SUPER_ADMIN_EMAILS: optional('SUPER_ADMIN_EMAIL', '').split(',').map(e => e.trim()).filter(Boolean), SUPER_ADMIN_EMAILS: optional('SUPER_ADMIN_EMAIL', '').split(',').map(e => e.trim()).filter(Boolean),
MINIO_ENDPOINT: required('MINIO_ENDPOINT'),
MINIO_PORT: parseInt(optional('MINIO_PORT', '443'), 10),
MINIO_USE_SSL: optional('MINIO_USE_SSL', 'true') === 'true',
MINIO_ACCESS_KEY: required('MINIO_ACCESS_KEY'),
MINIO_SECRET_KEY: required('MINIO_SECRET_KEY'),
MINIO_BUCKET: required('MINIO_BUCKET'),
MINIO_UPLOAD_DIR: optional('MINIO_UPLOAD_DIR', 'bug-reports'),
REDIS_URL: optional('REDIS_URL', ''),
} as const } as const

18
src/lib/logger.ts Normal file
View File

@@ -0,0 +1,18 @@
import { prisma } from './db'
import { LogType } from '../../generated/prisma'
export async function createSystemLog(userId: string, type: LogType, message: string) {
try {
return await prisma.log.create({
data: {
userId,
type,
message,
},
})
} catch (error) {
console.error('[Logger Error]', error)
// Don't throw, we don't want logging errors to break the main application flow
return null
}
}

36
src/lib/minio.ts Normal file
View File

@@ -0,0 +1,36 @@
import { Client } from 'minio'
import { env } from './env'
const client = new Client({
endPoint: env.MINIO_ENDPOINT,
port: env.MINIO_PORT,
useSSL: env.MINIO_USE_SSL,
accessKey: env.MINIO_ACCESS_KEY,
secretKey: env.MINIO_SECRET_KEY,
})
// Auto-create bucket if it doesn't exist
client.bucketExists(env.MINIO_BUCKET).then(async (exists) => {
if (!exists) {
await client.makeBucket(env.MINIO_BUCKET)
console.log(`[MinIO] Bucket "${env.MINIO_BUCKET}" created.`)
}
}).catch((err) => {
console.error('[MinIO] Failed to check/create bucket:', err.message)
})
export async function uploadBugImage(file: File): Promise<string> {
const ext = file.name.split('.').pop()?.toLowerCase() ?? 'bin'
const objectName = `${env.MINIO_UPLOAD_DIR}/${crypto.randomUUID()}.${ext}`
const buffer = Buffer.from(await file.arrayBuffer())
await client.putObject(env.MINIO_BUCKET, objectName, buffer, file.size, {
'Content-Type': file.type,
})
return objectName // e.g. bug-reports/uuid.jpg
}
export async function getMinioDownloadUrl(objectName: string): Promise<string> {
return client.presignedGetObject(env.MINIO_BUCKET, objectName, 3600)
}

44
src/lib/presence.ts Normal file
View File

@@ -0,0 +1,44 @@
import type { ServerWebSocket } from 'bun'
const connections = new Map<string, Set<ServerWebSocket<{ userId: string }>>>()
const adminSubs = new Set<ServerWebSocket<{ userId: string }>>()
export function getOnlineUserIds(): string[] {
return Array.from(connections.keys())
}
function broadcast() {
const online = getOnlineUserIds()
const msg = JSON.stringify({ type: 'presence', online })
for (const ws of adminSubs) ws.send(msg)
}
export function addConnection(ws: ServerWebSocket<{ userId: string }>, userId: string, isAdmin: boolean) {
let set = connections.get(userId)
if (!set) {
set = new Set()
connections.set(userId, set)
}
set.add(ws)
if (isAdmin) {
adminSubs.add(ws)
ws.send(JSON.stringify({ type: 'presence', online: getOnlineUserIds() }))
}
broadcast()
}
export function broadcastToAdmins(message: object) {
const msg = JSON.stringify(message)
for (const ws of adminSubs) ws.send(msg)
}
export function removeConnection(ws: ServerWebSocket<{ userId: string }>) {
const userId = ws.data.userId
const set = connections.get(userId)
if (set) {
set.delete(ws)
if (set.size === 0) connections.delete(userId)
}
adminSubs.delete(ws)
broadcast()
}

3
src/lib/redis.ts Normal file
View File

@@ -0,0 +1,3 @@
import { env } from './env'
export const redis = env.REDIS_URL ? new Bun.RedisClient(env.REDIS_URL) : null

104
src/lib/schema-parser.ts Normal file
View File

@@ -0,0 +1,104 @@
export interface SchemaField {
name: string
type: string
isId: boolean
isUnique: boolean
isOptional: boolean
isList: boolean
isRelation: boolean
default?: string
}
export interface SchemaRelation {
from: string
fromField: string
to: string
toField: string
onDelete?: string
}
export interface SchemaModel {
name: string
tableName: string
fields: SchemaField[]
}
export interface SchemaEnum {
name: string
values: string[]
}
export interface ParsedSchema {
models: SchemaModel[]
enums: SchemaEnum[]
relations: SchemaRelation[]
}
export function parseSchema(raw: string): ParsedSchema {
const models: SchemaModel[] = []
const enums: SchemaEnum[] = []
const relations: SchemaRelation[] = []
const blocks = raw.match(/(model|enum)\s+(\w+)\s*\{([^}]*)}/gs) ?? []
for (const block of blocks) {
const match = block.match(/(model|enum)\s+(\w+)\s*\{([^}]*)}/s)
if (!match) continue
const [, type, name, body] = match
const lines = body
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('//'))
if (type === 'enum') {
enums.push({ name, values: lines })
continue
}
let tableName = name
const fields: SchemaField[] = []
for (const line of lines) {
const mapMatch = line.match(/@@map\("(\w+)"\)/)
if (mapMatch) { tableName = mapMatch[1]; continue }
if (line.startsWith('@@')) continue
const fieldMatch = line.match(/^(\w+)\s+(\w+)(\?)?(\[\])?\s*(.*)$/)
if (!fieldMatch) continue
const [, fName, fType, optional, list, attrs] = fieldMatch
const isId = attrs.includes('@id')
const isUnique = attrs.includes('@unique')
const isRelation = attrs.includes('@relation')
const defaultMatch = attrs.match(/@default\(([^)]+)\)/)
const isModelRef =
/^[A-Z]/.test(fType) &&
!enums.some((e) => e.name === fType) &&
!['String', 'Int', 'Float', 'Boolean', 'DateTime', 'BigInt', 'Decimal', 'Bytes', 'Json'].includes(fType)
if (isRelation) {
const relMatch = attrs.match(
/@relation\(fields:\s*\[(\w+)],\s*references:\s*\[(\w+)](?:,\s*onDelete:\s*(\w+))?\)/,
)
if (relMatch) {
relations.push({ from: name, fromField: relMatch[1], to: fType, toField: relMatch[2], onDelete: relMatch[3] })
}
}
fields.push({
name: fName,
type: fType + (list ? '[]' : ''),
isId, isUnique,
isOptional: !!optional,
isList: !!list,
isRelation: isModelRef,
default: defaultMatch?.[1],
})
}
models.push({ name, tableName, fields })
}
return { models, enums, relations }
}

252
src/lib/seafile.ts Normal file
View File

@@ -0,0 +1,252 @@
#!/usr/bin/env bun
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
// --- Constants ---
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
// --- Types ---
interface Config {
TOKEN?: string;
REPO?: string;
URL?: string;
}
export const defaultConfigSF: Config = {
TOKEN: process.env.SF_TOKEN,
REPO: process.env.SF_REPO,
URL: process.env.SF_URL,
}
export async function loadConfig(): Promise<Config> {
if (!(await fs.stat(CONFIG_FILE)).isFile()) {
console.error(`⚠️ Config file not found at ${CONFIG_FILE}`);
console.error('Run: bun note.ts config to create/edit it.');
process.exit(1);
}
const configContent = await fs.readFile(CONFIG_FILE, 'utf8');
const config: Config = {};
configContent.split('\n').forEach((line) => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) return;
const [key, ...valueParts] = trimmed.split('=');
if (key && valueParts.length > 0) {
let value = valueParts.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
config[key as keyof Config] = value;
}
});
if (!config.TOKEN || !config.REPO || !config.URL) {
console.error(`❌ Config invalid. Please set TOKEN, REPO, and URL inside ${CONFIG_FILE}`);
process.exit(1);
}
return config;
}
// --- HTTP Helpers ---
export async function fetchWithAuth(config: Config, url: string, options: RequestInit = {}): Promise<Response> {
const headers = {
Authorization: `Token ${config.TOKEN}`,
...options.headers,
};
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
console.error(`❌ Request failed: ${response.status} ${response.statusText}`);
console.error(`🔍 URL: ${url}`);
console.error(`🔍 Headers:`, headers);
try {
const errorText = await response.text();
console.error(`🔍 Response body: ${errorText}`);
} catch {
console.error('🔍 Could not read response body');
}
}
return response;
}
// --- Commands ---
export async function testConnection(config: Config): Promise<string> {
try {
const response = await fetchWithAuth(config, `${config.URL}/ping/`);
return `✅ API connection successful: ${await response.text()}`
} catch {
// return '⚠️ API ping failed, trying repo access...'
try {
await fetchWithAuth(config, `${config.URL}/${config.REPO}/`);
return `✅ Repo access successful`
} catch {
return '❌ Both API ping and repo access failed'
}
}
}
export async function listFiles(config: Config): Promise<{ name: string }[]> {
const url = `${config.URL}/${config.REPO}/dir/?p=/`;
const response = await fetchWithAuth(config, url);
try {
const files = (await response.json()) as { name: string }[];
return files
} catch {
console.error('❌ Failed to parse response');
process.exit(1);
}
}
export async function catFile(config: Config, folder: string, fileName: string): Promise<ArrayBuffer> {
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`);
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
// Download file sebagai binary, BUKAN text
const fileResponse = await fetchWithAuth(config, downloadUrl);
const buffer = await fileResponse.arrayBuffer();
return buffer;
}
export async function uploadFile(config: Config, file: File, folder: string): Promise<string> {
const remoteName = path.basename(file.name);
// 1. Dapatkan upload link (pakai Authorization)
const uploadUrlResponse = await fetchWithAuth(
config,
`${config.URL}/${config.REPO}/upload-link/`
);
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
// 2. Siapkan form-data
const formData = new FormData();
formData.append("parent_dir", "/");
formData.append("relative_path", folder); // tanpa slash di akhir
formData.append("file", file, remoteName); // file langsung, jangan pakai Blob
// 3. Upload file TANPA Authorization header, token di query param
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
method: "POST",
body: formData,
});
const text = await res.text();
if (!res.ok) return 'gagal'
return `✅ Uploaded ${file.name} successfully`;
}
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string; }): Promise<string> {
const remoteName = path.basename(base64File.name);
// 1. Dapatkan upload link (pakai Authorization)
const uploadUrlResponse = await fetchWithAuth(
config,
`${config.URL}/${config.REPO}/upload-link/`
);
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
// 2. Konversi base64 ke Blob
const binary = Buffer.from(base64File.data, "base64");
const blob = new Blob([binary]);
// 3. Siapkan form-data
const formData = new FormData();
formData.append("parent_dir", "/");
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
formData.append("file", blob, remoteName);
// 4. Upload file TANPA Authorization header, token di query param
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
method: "POST",
body: formData,
});
const text = await res.text();
if (!res.ok) throw new Error(`Upload failed: ${text}`);
return `✅ Uploaded ${base64File.name} successfully`;
}
export async function uploadFileToFolder(config: Config, base64File: { name: string; data: string; }, folder: 'syarat-dokumen' | 'pengaduan'): Promise<string> {
const remoteName = path.basename(base64File.name);
// 1. Dapatkan upload link (pakai Authorization)
const uploadUrlResponse = await fetchWithAuth(
config,
`${config.URL}/${config.REPO}/upload-link/`
);
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
// 2. Konversi base64 ke Blob
const binary = Buffer.from(base64File.data, "base64");
const blob = new Blob([binary]);
// 3. Siapkan form-data
const formData = new FormData();
formData.append("parent_dir", "/");
formData.append("relative_path", folder); // tanpa slash di akhir
formData.append("file", blob, remoteName);
// 4. Upload file TANPA Authorization header, token di query param
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
method: "POST",
body: formData,
});
const text = await res.text();
if (!res.ok) throw new Error(`Upload failed: ${text}`);
return `✅ Uploaded ${base64File.name} successfully`;
}
export async function removeFile(config: Config, fileName: string, folder: string): Promise<string> {
const res = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`, { method: 'DELETE' });
if (!res.ok) return 'gagal menghapus file';
return `🗑️ Removed ${fileName}`
}
export async function moveFile(config: Config, oldName: string, newName: string): Promise<string> {
const url = `${config.URL}/${config.REPO}/file/?p=/${oldName}`;
const formData = new FormData();
formData.append('operation', 'rename');
formData.append('newname', newName);
await fetchWithAuth(config, url, { method: 'POST', body: formData });
return `✏️ Renamed ${oldName}${newName}`
}
export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise<string> {
const localName = localFile || fileName;
// 🔹 gabungkan path folder + file
const filePath = `/${folder}/${fileName}`.replace(/\/+/g, "/");
// 🔹 encode path agar aman (spasi, dll)
const params = new URLSearchParams({
p: filePath,
});
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?${params.toString()}`);
if (!downloadUrlResponse.ok)
return 'gagal'
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
await fs.writeFile(localName, buffer);
return `⬇️ Downloaded ${fileName}${localName}`
}
export async function getFileLink(config: Config, fileName: string): Promise<string> {
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`);
return `🔗 Link for ${fileName}:\n${(await downloadUrlResponse.text()).replace(/"/g, '')}`
}

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 },