Compare commits
17 Commits
cd295abf2c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b6e3f3430 | |||
| b03f267743 | |||
| 94724a5081 | |||
| 373198c7c4 | |||
| 9d80eb3b85 | |||
| 8527671f46 | |||
| 1104217070 | |||
| 786953054a | |||
| 9c725fa230 | |||
|
|
3ec6383535 | ||
| 8f6a68b9f1 | |||
| 63c0a6acff | |||
| a0ca6be8e1 | |||
| eb77aca715 | |||
| 041d891a8d | |||
| de5ec1af93 | |||
| d09a702d64 |
@@ -16,6 +16,9 @@ GOOGLE_CLIENT_SECRET=
|
|||||||
# Role
|
# Role
|
||||||
SUPER_ADMIN_EMAIL=admin@example.com
|
SUPER_ADMIN_EMAIL=admin@example.com
|
||||||
|
|
||||||
|
# API Key for external clients (e.g. mobile apps)
|
||||||
|
API_KEY=your-secret-api-key-here
|
||||||
|
|
||||||
# Telegram Notification (optional)
|
# Telegram Notification (optional)
|
||||||
TELEGRAM_NOTIFY_TOKEN=
|
TELEGRAM_NOTIFY_TOKEN=
|
||||||
TELEGRAM_NOTIFY_CHAT_ID=
|
TELEGRAM_NOTIFY_CHAT_ID=
|
||||||
|
|||||||
106
.github/workflows/publish.yml
vendored
Normal file
106
.github/workflows/publish.yml
vendored
Normal 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
60
.github/workflows/re-pull.yml
vendored
Normal 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
26
.github/workflows/script/notify.sh
vendored
Normal 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
120
.github/workflows/script/re-pull.sh
vendored
Normal 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
|
||||||
102
CLAUDE.md
102
CLAUDE.md
@@ -1,82 +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`.
|
|
||||||
|
|
||||||
## Playwright MCP
|
|
||||||
|
|
||||||
Playwright MCP server enables AI-assisted browser automation for testing and debugging.
|
|
||||||
|
|
||||||
- MCP config: `.qwen/settings.json` — Qwen Code auto-loads on session start
|
|
||||||
- Playwright config: `playwright.config.ts` — E2E test configuration
|
|
||||||
- Run manually: `bun run mcp:playwright` — starts headless browser MCP server
|
|
||||||
- Install browsers: `bunx playwright install` — downloads Chromium and other browsers
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Tests use `bun:test`. Three levels:
|
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
|
|
||||||
|
|||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
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"]
|
||||||
168
bun.lock
168
bun.lock
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bun-react-template",
|
"name": "bun-react-template",
|
||||||
@@ -8,13 +7,19 @@
|
|||||||
"@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",
|
||||||
|
"@elysiajs/swagger": "^1.3.1",
|
||||||
"@mantine/charts": "^9.0.0",
|
"@mantine/charts": "^9.0.0",
|
||||||
"@mantine/core": "^8.3.18",
|
"@mantine/core": "^8.3.18",
|
||||||
"@mantine/hooks": "^8.3.18",
|
"@mantine/hooks": "^8.3.18",
|
||||||
|
"@mantine/modals": "^8.3.18",
|
||||||
|
"@mantine/notifications": "^8.3.18",
|
||||||
"@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",
|
||||||
|
"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 +31,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 +113,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=="],
|
||||||
@@ -193,10 +203,22 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"@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 +269,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 +319,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 +329,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,8 +351,14 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -331,6 +373,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,11 +397,15 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -375,6 +423,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=="],
|
||||||
@@ -401,6 +451,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 +465,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,12 +475,18 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -443,12 +505,16 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -487,6 +553,10 @@
|
|||||||
|
|
||||||
"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-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 +565,9 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -515,6 +587,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -523,10 +597,14 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -571,10 +649,20 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
@@ -591,6 +679,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -601,7 +691,9 @@
|
|||||||
|
|
||||||
"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=="],
|
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
@@ -613,6 +705,10 @@
|
|||||||
|
|
||||||
"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 +729,8 @@
|
|||||||
|
|
||||||
"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-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 +741,8 @@
|
|||||||
|
|
||||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"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 +765,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=="],
|
||||||
@@ -683,6 +787,10 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
@@ -701,12 +809,24 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"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 +843,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=="],
|
||||||
@@ -779,6 +901,10 @@
|
|||||||
|
|
||||||
"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 +915,12 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"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,30 +929,60 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|||||||
65
compose.yml
Normal file
65
compose.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
services:
|
||||||
|
monitoring-app:
|
||||||
|
image: ghcr.io/bipprojectbali/monitoring-app:stg-latest
|
||||||
|
container_name: monitoring-app-stg
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Database
|
||||||
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
- DIRECT_URL=${DIRECT_URL}
|
||||||
|
# Google OAuth
|
||||||
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||||
|
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||||
|
# App
|
||||||
|
- PORT=${PORT:-3000}
|
||||||
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
|
# Admin (initial Super Admin emails, comma-separated)
|
||||||
|
- SUPER_ADMIN_EMAIL=${SUPER_ADMIN_EMAIL}
|
||||||
|
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
66
docs/ARCHITECTURE.md
Normal 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
6
docs/DEV_TOOLS.md
Normal 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
6
docs/TESTING.md
Normal 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()`
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"@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",
|
||||||
|
"@elysiajs/swagger": "^1.3.1",
|
||||||
"@mantine/charts": "^9.0.0",
|
"@mantine/charts": "^9.0.0",
|
||||||
"@mantine/core": "^8.3.18",
|
"@mantine/core": "^8.3.18",
|
||||||
"@mantine/hooks": "^8.3.18",
|
"@mantine/hooks": "^8.3.18",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"@tanstack/react-query": "^5.95.2",
|
"@tanstack/react-query": "^5.95.2",
|
||||||
"@tanstack/react-router": "^1.168.10",
|
"@tanstack/react-router": "^1.168.10",
|
||||||
"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",
|
||||||
@@ -40,7 +42,10 @@
|
|||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
"swr": "^2.4.1"
|
"swr": "^2.4.1",
|
||||||
|
"@mantine/modals": "^8.3.18",
|
||||||
|
"@xyflow/react": "^12.6.4",
|
||||||
|
"elkjs": "^0.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.10",
|
"@biomejs/biome": "^2.4.10",
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -9,6 +9,7 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
|
USER
|
||||||
ADMIN
|
ADMIN
|
||||||
DEVELOPER
|
DEVELOPER
|
||||||
}
|
}
|
||||||
@@ -41,8 +42,9 @@ 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?
|
||||||
role Role @default(ADMIN)
|
googleId String? @unique
|
||||||
|
role Role @default(USER)
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
image String?
|
image String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
1062
src/app.ts
1062
src/app.ts
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
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/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 +65,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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
import { APP_CONFIGS } from '@/frontend/config/appMenus'
|
import { APP_CONFIGS } from '@/frontend/config/appMenus'
|
||||||
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
|
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,
|
Loader,
|
||||||
|
LoadingOverlay,
|
||||||
Menu,
|
Menu,
|
||||||
NavLink,
|
NavLink,
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
useComputedColorScheme,
|
useComputedColorScheme,
|
||||||
useMantineColorScheme
|
useMantineColorScheme
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
@@ -26,6 +31,7 @@ import {
|
|||||||
TbApps,
|
TbApps,
|
||||||
TbArrowLeft,
|
TbArrowLeft,
|
||||||
TbChevronRight,
|
TbChevronRight,
|
||||||
|
TbClock,
|
||||||
TbDashboard,
|
TbDashboard,
|
||||||
TbDeviceMobile,
|
TbDeviceMobile,
|
||||||
TbHistory,
|
TbHistory,
|
||||||
@@ -54,10 +60,17 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
const currentPath = matches[matches.length - 1]?.pathname
|
const currentPath = matches[matches.length - 1]?.pathname
|
||||||
|
|
||||||
// ─── Connect to auth system ──────────────────────────
|
// ─── Connect to auth system ──────────────────────────
|
||||||
const { data: sessionData } = useSession()
|
const { data: sessionData, isLoading: sessionLoading } = useSession()
|
||||||
const user = sessionData?.user
|
const user = sessionData?.user
|
||||||
const logout = useLogout()
|
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 ─────────────
|
// ─── Fetch registered apps from database ─────────────
|
||||||
const { data: appsData } = useQuery({
|
const { data: appsData } = useQuery({
|
||||||
queryKey: ['apps'],
|
queryKey: ['apps'],
|
||||||
@@ -99,6 +112,15 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
logout.mutate()
|
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 }}
|
||||||
@@ -263,16 +285,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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
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 { 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'
|
||||||
|
|
||||||
export interface ErrorDataTableProps {
|
export interface ErrorDataTableProps {
|
||||||
appId?: string
|
appId?: string
|
||||||
@@ -26,6 +26,7 @@ export interface ErrorDataTableProps {
|
|||||||
export function ErrorDataTable({ appId }: ErrorDataTableProps) {
|
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({
|
const { data: bugsData, isLoading } = useQuery({
|
||||||
queryKey: ['bugs', appId],
|
queryKey: ['bugs', appId],
|
||||||
@@ -36,11 +37,12 @@ export function ErrorDataTable({ appId }: ErrorDataTableProps) {
|
|||||||
|
|
||||||
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?.toUpperCase()) {
|
switch (sev?.toUpperCase()) {
|
||||||
case 'OPEN': return 'red'
|
case 'OPEN': return 'red'
|
||||||
case 'IN_PROGRESS': return 'orange'
|
case 'IN_PROGRESS': return 'orange'
|
||||||
case 'ON_HOLD': return 'yellow'
|
case 'ON_HOLD': return 'yellow'
|
||||||
@@ -91,9 +93,9 @@ export function ErrorDataTable({ appId }: ErrorDataTableProps) {
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
) : bugs.map((error: any) => (
|
) : bugs.map((error: any) => (
|
||||||
<Table.Tr
|
<Table.Tr
|
||||||
key={error.id}
|
key={error.id}
|
||||||
onClick={() => handleRowClick(error)}
|
onClick={() => handleRowClick(error)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<Table.Td px="xl">
|
<Table.Td px="xl">
|
||||||
<Text size="sm" fw={600} lineClamp={1}>{error.description}</Text>
|
<Text size="sm" fw={600} lineClamp={1}>{error.description}</Text>
|
||||||
@@ -107,7 +109,7 @@ export function ErrorDataTable({ appId }: ErrorDataTableProps) {
|
|||||||
<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">{new Date(error.createdAt).toLocaleString()}</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">
|
||||||
@@ -146,8 +148,8 @@ export function ErrorDataTable({ appId }: ErrorDataTableProps) {
|
|||||||
|
|
||||||
<SimpleGrid cols={2} spacing="lg">
|
<SimpleGrid cols={2} spacing="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" fw={700} c="dimmed" mb={4}>REPORTER</Text>
|
<Text size="xs" fw={700} c="dimmed" mb={4}>SOURCE</Text>
|
||||||
<Text fw={600}>{selectedError.user?.name || selectedError.userId || 'System'}</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>
|
||||||
@@ -158,16 +160,23 @@ export function ErrorDataTable({ appId }: ErrorDataTableProps) {
|
|||||||
<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>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const API_URLS = {
|
|||||||
getBugs: (page: number, search: string, app: string, status: string) =>
|
getBugs: (page: number, search: string, app: string, status: string) =>
|
||||||
`/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`,
|
`/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`,
|
||||||
createBug: () => `/api/bugs`,
|
createBug: () => `/api/bugs`,
|
||||||
|
uploadImage: () => `/api/upload/image`,
|
||||||
updateBugStatus: (id: string) => `/api/bugs/${id}/status`,
|
updateBugStatus: (id: string) => `/api/bugs/${id}/status`,
|
||||||
updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`,
|
updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`,
|
||||||
createLog: () => `/api/logs`,
|
createLog: () => `/api/logs`,
|
||||||
|
|||||||
@@ -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 = | 'ADMIN' | 'DEVELOPER'
|
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,7 +48,7 @@ export function useLogin() {
|
|||||||
}),
|
}),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.setQueryData(['auth', 'session'], data)
|
queryClient.setQueryData(['auth', 'session'], data)
|
||||||
navigate({ to: '/dashboard' })
|
navigate({ to: getDefaultRoute(data.user.role) })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/frontend/hooks/usePresence.ts
Normal file
42
src/frontend/hooks/usePresence.ts
Normal 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 }
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Code,
|
Code,
|
||||||
Collapse,
|
Collapse,
|
||||||
|
FileInput,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
Loader,
|
Loader,
|
||||||
@@ -57,11 +58,16 @@ function AppErrorsPage() {
|
|||||||
|
|
||||||
|
|
||||||
const [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
|
const [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
|
||||||
|
const [showStackTrace, setShowStackTrace] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
const toggleLogs = (bugId: string) => {
|
const toggleLogs = (bugId: string) => {
|
||||||
setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
|
setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleStackTrace = (bugId: string) => {
|
||||||
|
setShowStackTrace((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
|
||||||
|
}
|
||||||
|
|
||||||
const { data, isLoading, refetch } = useQuery({
|
const { data, isLoading, refetch } = useQuery({
|
||||||
queryKey: ['bugs', { page, search, app, status }],
|
queryKey: ['bugs', { page, search, app, status }],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -74,19 +80,21 @@ function AppErrorsPage() {
|
|||||||
queryFn: () => fetch('/api/apps').then((r) => r.json()),
|
queryFn: () => fetch('/api/apps').then((r) => r.json()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Image Preview
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null)
|
||||||
|
|
||||||
// Create Bug Modal Logic
|
// Create Bug Modal Logic
|
||||||
const [opened, { open, close }] = useDisclosure(false)
|
const [opened, { open, close }] = useDisclosure(false)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [imageFiles, setImageFiles] = useState<File[]>([])
|
||||||
const [createForm, setCreateForm] = useState({
|
const [createForm, setCreateForm] = useState({
|
||||||
description: '',
|
description: '',
|
||||||
app: appId,
|
app: appId,
|
||||||
status: 'OPEN',
|
|
||||||
source: 'USER',
|
source: 'USER',
|
||||||
affectedVersion: '',
|
affectedVersion: '',
|
||||||
device: '',
|
device: '',
|
||||||
os: '',
|
os: '',
|
||||||
stackTrace: '',
|
stackTrace: '',
|
||||||
imageUrl: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update Status Modal Logic
|
// Update Status Modal Logic
|
||||||
@@ -189,10 +197,20 @@ function AppErrorsPage() {
|
|||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
try {
|
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(), {
|
const res = await fetch(API_URLS.createBug(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(createForm),
|
body: JSON.stringify({ ...createForm, imageUrls: imageUrls.length ? imageUrls : undefined }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -210,16 +228,15 @@ function AppErrorsPage() {
|
|||||||
})
|
})
|
||||||
refetch()
|
refetch()
|
||||||
close()
|
close()
|
||||||
|
setImageFiles([])
|
||||||
setCreateForm({
|
setCreateForm({
|
||||||
description: '',
|
description: '',
|
||||||
app: 'desa_plus',
|
app: appId,
|
||||||
status: 'OPEN',
|
|
||||||
source: 'USER',
|
source: 'USER',
|
||||||
affectedVersion: '',
|
affectedVersion: '',
|
||||||
device: '',
|
device: '',
|
||||||
os: '',
|
os: '',
|
||||||
stackTrace: '',
|
stackTrace: '',
|
||||||
imageUrl: '',
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to create error report')
|
throw new Error('Failed to create error report')
|
||||||
@@ -256,6 +273,28 @@ function AppErrorsPage() {
|
|||||||
</Button>
|
</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
|
<Modal
|
||||||
opened={updateModalOpened}
|
opened={updateModalOpened}
|
||||||
onClose={closeUpdateModal}
|
onClose={closeUpdateModal}
|
||||||
@@ -331,7 +370,7 @@ function AppErrorsPage() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={close}
|
onClose={() => { close(); setImageFiles([]); }}
|
||||||
title={<Text fw={700} size="lg">Report New Error</Text>}
|
title={<Text fw={700} size="lg">Report New Error</Text>}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -368,25 +407,13 @@ function AppErrorsPage() {
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<SimpleGrid cols={2}>
|
<TextInput
|
||||||
<TextInput
|
label="Version"
|
||||||
label="Version"
|
placeholder="e.g. 2.4.1"
|
||||||
placeholder="e.g. 2.4.1"
|
required
|
||||||
required
|
value={createForm.affectedVersion}
|
||||||
value={createForm.affectedVersion}
|
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
|
||||||
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
|
/>
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label="Initial Status"
|
|
||||||
data={[
|
|
||||||
{ value: 'OPEN', label: 'Open' },
|
|
||||||
{ value: 'ON_HOLD', label: 'On Hold' },
|
|
||||||
{ value: 'IN_PROGRESS', label: 'In Progress' },
|
|
||||||
]}
|
|
||||||
value={createForm.status}
|
|
||||||
onChange={(val) => setCreateForm({ ...createForm, status: val as any })}
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<SimpleGrid cols={2}>
|
<SimpleGrid cols={2}>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -405,11 +432,22 @@ function AppErrorsPage() {
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<TextInput
|
<FileInput
|
||||||
label="Image URL (Optional)"
|
label="Screenshot (Optional)"
|
||||||
placeholder="https://example.com/screenshot.png"
|
placeholder="Klik untuk upload gambar..."
|
||||||
value={createForm.imageUrl}
|
accept="image/*"
|
||||||
onChange={(e) => setCreateForm({ ...createForm, imageUrl: e.target.value })}
|
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
|
<Textarea
|
||||||
@@ -525,7 +563,7 @@ function AppErrorsPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{new Date(bug.createdAt).toLocaleString()} • {bug.app?.toUpperCase()} • v{bug.affectedVersion}
|
{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>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -578,19 +616,31 @@ function AppErrorsPage() {
|
|||||||
{/* Stack Trace */}
|
{/* Stack Trace */}
|
||||||
{bug.stackTrace && (
|
{bug.stackTrace && (
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" fw={700} c="dimmed" mb={4}>STACK TRACE</Text>
|
<Group justify="space-between" mb={4}>
|
||||||
<Code
|
<Text size="xs" fw={700} c="dimmed">STACK TRACE</Text>
|
||||||
block
|
<Button
|
||||||
color="red"
|
variant="subtle"
|
||||||
style={{
|
size="compact-xs"
|
||||||
fontFamily: 'monospace',
|
color="gray"
|
||||||
whiteSpace: 'pre-wrap',
|
onClick={() => toggleStackTrace(bug.id)}
|
||||||
fontSize: '11px',
|
>
|
||||||
border: '1px solid var(--mantine-color-default-border)',
|
{showStackTrace[bug.id] ? 'Hide' : 'Show'}
|
||||||
}}
|
</Button>
|
||||||
>
|
</Group>
|
||||||
{bug.stackTrace}
|
<Collapse in={!!showStackTrace[bug.id]}>
|
||||||
</Code>
|
<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>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -603,7 +653,13 @@ function AppErrorsPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
|
||||||
{bug.images.map((img: any) => (
|
{bug.images.map((img: any) => (
|
||||||
<Paper key={img.id} withBorder radius="md" style={{ overflow: 'hidden' }}>
|
<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" />
|
<Image src={img.imageUrl} alt="Error screenshot" height={100} fit="cover" />
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -73,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">
|
||||||
@@ -430,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">
|
||||||
@@ -444,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}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
|
FileInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
Textarea,
|
Textarea,
|
||||||
Title,
|
Title,
|
||||||
@@ -55,10 +56,14 @@ function ListErrorsPage() {
|
|||||||
const [status, setStatus] = useState('all')
|
const [status, setStatus] = useState('all')
|
||||||
|
|
||||||
const [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
|
const [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
|
||||||
|
const [showStackTrace, setShowStackTrace] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
const toggleLogs = (bugId: string) => {
|
const toggleLogs = (bugId: string) => {
|
||||||
setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
|
setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
|
||||||
}
|
}
|
||||||
|
const toggleStackTrace = (bugId: string) => {
|
||||||
|
setShowStackTrace((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
|
||||||
|
}
|
||||||
|
|
||||||
const { data, isLoading, refetch } = useQuery({
|
const { data, isLoading, refetch } = useQuery({
|
||||||
queryKey: ['bugs', { page, search, app, status }],
|
queryKey: ['bugs', { page, search, app, status }],
|
||||||
@@ -72,19 +77,21 @@ function ListErrorsPage() {
|
|||||||
queryFn: () => fetch('/api/apps').then((r) => r.json()),
|
queryFn: () => fetch('/api/apps').then((r) => r.json()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Image Preview
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null)
|
||||||
|
|
||||||
// Create Bug Modal Logic
|
// Create Bug Modal Logic
|
||||||
const [opened, { open, close }] = useDisclosure(false)
|
const [opened, { open, close }] = useDisclosure(false)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [imageFiles, setImageFiles] = useState<File[]>([])
|
||||||
const [createForm, setCreateForm] = useState({
|
const [createForm, setCreateForm] = useState({
|
||||||
description: '',
|
description: '',
|
||||||
app: 'desa-plus',
|
app: 'desa-plus',
|
||||||
status: 'OPEN',
|
|
||||||
source: 'USER',
|
source: 'USER',
|
||||||
affectedVersion: '',
|
affectedVersion: '',
|
||||||
device: '',
|
device: '',
|
||||||
os: '',
|
os: '',
|
||||||
stackTrace: '',
|
stackTrace: '',
|
||||||
imageUrl: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update Status Modal Logic
|
// Update Status Modal Logic
|
||||||
@@ -187,10 +194,20 @@ function ListErrorsPage() {
|
|||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
try {
|
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(), {
|
const res = await fetch(API_URLS.createBug(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(createForm),
|
body: JSON.stringify({ ...createForm, imageUrls: imageUrls.length ? imageUrls : undefined }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -208,16 +225,15 @@ function ListErrorsPage() {
|
|||||||
})
|
})
|
||||||
refetch()
|
refetch()
|
||||||
close()
|
close()
|
||||||
|
setImageFiles([])
|
||||||
setCreateForm({
|
setCreateForm({
|
||||||
description: '',
|
description: '',
|
||||||
app: 'desa-plus',
|
app: 'desa-plus',
|
||||||
status: 'OPEN',
|
|
||||||
source: 'USER',
|
source: 'USER',
|
||||||
affectedVersion: '',
|
affectedVersion: '',
|
||||||
device: '',
|
device: '',
|
||||||
os: '',
|
os: '',
|
||||||
stackTrace: '',
|
stackTrace: '',
|
||||||
imageUrl: '',
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to create error report')
|
throw new Error('Failed to create error report')
|
||||||
@@ -265,6 +281,28 @@ function ListErrorsPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
</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
|
<Modal
|
||||||
opened={updateModalOpened}
|
opened={updateModalOpened}
|
||||||
onClose={closeUpdateModal}
|
onClose={closeUpdateModal}
|
||||||
@@ -340,7 +378,7 @@ function ListErrorsPage() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={close}
|
onClose={() => { close(); setImageFiles([]); }}
|
||||||
title={<Text fw={700} size="lg">Report New Error</Text>}
|
title={<Text fw={700} size="lg">Report New Error</Text>}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -377,25 +415,13 @@ function ListErrorsPage() {
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<SimpleGrid cols={2}>
|
<TextInput
|
||||||
<TextInput
|
label="Version"
|
||||||
label="Version"
|
placeholder="e.g. 2.4.1"
|
||||||
placeholder="e.g. 2.4.1"
|
required
|
||||||
required
|
value={createForm.affectedVersion}
|
||||||
value={createForm.affectedVersion}
|
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
|
||||||
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
|
/>
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label="Initial Status"
|
|
||||||
data={[
|
|
||||||
{ value: 'OPEN', label: 'Open' },
|
|
||||||
{ value: 'ON_HOLD', label: 'On Hold' },
|
|
||||||
{ value: 'IN_PROGRESS', label: 'In Progress' },
|
|
||||||
]}
|
|
||||||
value={createForm.status}
|
|
||||||
onChange={(val) => setCreateForm({ ...createForm, status: val as any })}
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<SimpleGrid cols={2}>
|
<SimpleGrid cols={2}>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -414,11 +440,22 @@ function ListErrorsPage() {
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<TextInput
|
<FileInput
|
||||||
label="Image URL (Optional)"
|
label="Screenshot (Optional)"
|
||||||
placeholder="https://example.com/screenshot.png"
|
placeholder="Klik untuk upload gambar..."
|
||||||
value={createForm.imageUrl}
|
accept="image/*"
|
||||||
onChange={(e) => setCreateForm({ ...createForm, imageUrl: e.target.value })}
|
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
|
<Textarea
|
||||||
@@ -545,7 +582,7 @@ function ListErrorsPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{new Date(bug.createdAt).toLocaleString()} • {bug.app?.toUpperCase()} • v{bug.affectedVersion}
|
{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>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -598,19 +635,31 @@ function ListErrorsPage() {
|
|||||||
{/* Stack Trace */}
|
{/* Stack Trace */}
|
||||||
{bug.stackTrace && (
|
{bug.stackTrace && (
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" fw={700} c="dimmed" mb={4}>STACK TRACE</Text>
|
<Group justify="space-between" mb={showStackTrace[bug.id] ? 8 : 0}>
|
||||||
<Code
|
<Text size="xs" fw={700} c="dimmed">STACK TRACE</Text>
|
||||||
block
|
<Button
|
||||||
color="red"
|
variant="subtle"
|
||||||
style={{
|
size="compact-xs"
|
||||||
fontFamily: 'monospace',
|
color="gray"
|
||||||
whiteSpace: 'pre-wrap',
|
onClick={() => toggleStackTrace(bug.id)}
|
||||||
fontSize: '11px',
|
>
|
||||||
border: '1px solid var(--mantine-color-default-border)',
|
{showStackTrace[bug.id] ? 'Hide' : 'Show'}
|
||||||
}}
|
</Button>
|
||||||
>
|
</Group>
|
||||||
{bug.stackTrace}
|
<Collapse in={showStackTrace[bug.id]}>
|
||||||
</Code>
|
<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>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -623,7 +672,13 @@ function ListErrorsPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
|
||||||
{bug.images.map((img: any) => (
|
{bug.images.map((img: any) => (
|
||||||
<Paper key={img.id} withBorder radius="md" style={{ overflow: 'hidden' }}>
|
<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" />
|
<Image src={img.imageUrl} alt="Error screenshot" height={100} fit="cover" />
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
|
|||||||
1481
src/frontend/routes/dev.tsx
Normal file
1481
src/frontend/routes/dev.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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: '/dashboard' })
|
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
|
||||||
@@ -59,7 +60,14 @@ function LoginPage() {
|
|||||||
|
|
||||||
{(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.',
|
||||||
|
}[searchError ?? ''] ?? 'Login dengan Google gagal, silakan coba lagi.'
|
||||||
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -89,6 +97,17 @@ function LoginPage() {
|
|||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Divider label="or" labelPosition="center" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
fullWidth
|
||||||
|
leftSection={<FcGoogle size={18} />}
|
||||||
|
onClick={() => { window.location.href = '/api/auth/google' }}
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -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,6 +31,7 @@ export const Route = createFileRoute('/profile')({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const roleBadgeColor: Record<string, string> = {
|
const roleBadgeColor: Record<string, string> = {
|
||||||
|
USER: 'gray',
|
||||||
ADMIN: 'violet',
|
ADMIN: 'violet',
|
||||||
DEVELOPER: 'red',
|
DEVELOPER: 'red',
|
||||||
}
|
}
|
||||||
@@ -55,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' }}>
|
||||||
|
|||||||
@@ -50,10 +50,8 @@ export const Route = createFileRoute('/users')({
|
|||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||||
|
|
||||||
const getRoleColor = (role: string) => {
|
const getRoleColor = (role: string) => {
|
||||||
const r = (role || '').toLowerCase()
|
if (role === 'DEVELOPER') return 'violet'
|
||||||
if (r.includes('super')) return 'red'
|
if (role === 'ADMIN') return 'brand-blue'
|
||||||
if (r.includes('admin')) return 'brand-blue'
|
|
||||||
if (r.includes('developer')) return 'violet'
|
|
||||||
return 'gray'
|
return 'gray'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +95,7 @@ function UsersPage() {
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
role: 'USER',
|
role: 'ADMIN',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCreateUser = async () => {
|
const handleCreateUser = async () => {
|
||||||
@@ -119,7 +117,7 @@ function UsersPage() {
|
|||||||
mutateOperators()
|
mutateOperators()
|
||||||
mutateStats()
|
mutateStats()
|
||||||
closeCreate()
|
closeCreate()
|
||||||
setCreateForm({ name: '', email: '', password: '', role: 'USER' })
|
setCreateForm({ name: '', email: '', password: '', role: 'ADMIN' })
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json()
|
const err = await res.json()
|
||||||
throw new Error(err.error || 'Failed to create user')
|
throw new Error(err.error || 'Failed to create user')
|
||||||
@@ -367,9 +365,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>
|
||||||
))}
|
))}
|
||||||
@@ -457,11 +455,12 @@ function UsersPage() {
|
|||||||
<Select
|
<Select
|
||||||
label="Role"
|
label="Role"
|
||||||
data={[
|
data={[
|
||||||
|
{ value: 'USER', label: 'User (Pending)' },
|
||||||
{ value: 'ADMIN', label: 'Admin' },
|
{ value: 'ADMIN', label: 'Admin' },
|
||||||
{ value: 'DEVELOPER', label: 'Developer' },
|
{ value: 'DEVELOPER', label: 'Developer' },
|
||||||
]}
|
]}
|
||||||
value={editForm.role}
|
value={editForm.role}
|
||||||
onChange={(val) => setEditForm({ ...editForm, role: val || 'USER' })}
|
onChange={(val) => setEditForm({ ...editForm, role: val || 'ADMIN' })}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
45
src/lib/applog.ts
Normal 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)
|
||||||
|
}
|
||||||
@@ -12,8 +12,18 @@ 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),
|
||||||
|
API_KEY: required('API_KEY'),
|
||||||
|
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
|
||||||
|
|||||||
36
src/lib/minio.ts
Normal file
36
src/lib/minio.ts
Normal 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
44
src/lib/presence.ts
Normal 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
3
src/lib/redis.ts
Normal 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
104
src/lib/schema-parser.ts
Normal 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
252
src/lib/seafile.ts
Normal 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, '')}`
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user