mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-20 02:41:01 +00:00
Compare commits
14 Commits
7fea37eb5d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 070d682759 | |||
| cd56ab8fb6 | |||
| b012fcaaac | |||
| 7a1d3e7917 | |||
| 2ee3caac0d | |||
| 78e93e9766 | |||
| 83d66feacc | |||
| d6ace14e70 | |||
| 2cbfd21539 | |||
| f7c070e45b | |||
| 06ed9734c6 | |||
| d93d4c6cd0 | |||
| faba2dee5d | |||
| ca3976fe62 |
@@ -0,0 +1,41 @@
|
|||||||
|
# pwap/web Docker build context (root) — exclude everything not needed
|
||||||
|
# for `web/` build. Other monorepo subprojects stay out of the image.
|
||||||
|
|
||||||
|
# Other monorepo dirs (we only need web/ + shared/)
|
||||||
|
exchange/
|
||||||
|
mobile/
|
||||||
|
pwap-mobile/
|
||||||
|
docs/
|
||||||
|
res/
|
||||||
|
|
||||||
|
# All node_modules everywhere
|
||||||
|
**/node_modules/
|
||||||
|
**/dist/
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
# Git, GitHub
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# Env files (built-in vars are passed as build-args from CI)
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
|
# Editor / OS
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
**/.eslintcache
|
||||||
|
**/coverage/
|
||||||
|
|
||||||
|
# Already-built artifacts (we rebuild fresh inside container)
|
||||||
|
web/dist/
|
||||||
|
shared/**/dist/
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: CodeQL (SAST)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
schedule:
|
||||||
|
# Every Sunday at 02:00 UTC — catches CVEs disclosed during the week
|
||||||
|
- cron: '0 2 * * 0'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: codeql-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze ${{ matrix.language }}
|
||||||
|
runs-on: pwap-runner
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [javascript-typescript]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# OWASP top-10 + injection + auth flaws + prototype pollution
|
||||||
|
queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
- name: Perform analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: /language:${{ matrix.language }}
|
||||||
|
# GitHub Advanced Security dashboard upload requires paid plan.
|
||||||
|
# SARIF saved as a downloadable artifact instead.
|
||||||
|
upload: false
|
||||||
|
output: /tmp/codeql-results
|
||||||
|
|
||||||
|
- name: Upload SARIF as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: codeql-sarif-${{ matrix.language }}
|
||||||
|
path: /tmp/codeql-results/*.sarif
|
||||||
|
retention-days: 7
|
||||||
@@ -6,14 +6,25 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, develop ]
|
branches: [ main, develop ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
rollback_to:
|
||||||
|
description: 'Rollback to git SHA (skips build, redeploys old image). Empty = normal deploy.'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # version bump commit
|
||||||
|
packages: write # GHCR push
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
|
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
|
||||||
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: pezkuwichain/pwap-web
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ========================================
|
# ========================================
|
||||||
@@ -21,7 +32,7 @@ jobs:
|
|||||||
# ========================================
|
# ========================================
|
||||||
web:
|
web:
|
||||||
name: Web App
|
name: Web App
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -74,14 +85,163 @@ jobs:
|
|||||||
name: web-dist
|
name: web-dist
|
||||||
path: web/dist/
|
path: web/dist/
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# BUILD & PUSH DOCKER IMAGE TO GHCR
|
||||||
|
# Immutable artifact for audit + rollback (vs ephemeral GHA artifact).
|
||||||
|
# Tagged with git SHA so any commit can be redeployed by SHA.
|
||||||
|
# ========================================
|
||||||
|
build-image:
|
||||||
|
name: Build & Push Image
|
||||||
|
runs-on: pwap-runner
|
||||||
|
needs: [web, telegram-gate]
|
||||||
|
if: |
|
||||||
|
github.ref == 'refs/heads/main' &&
|
||||||
|
(github.event_name == 'push' ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write # cosign keyless signing via Sigstore OIDC
|
||||||
|
outputs:
|
||||||
|
image_sha: ${{ steps.meta.outputs.image_sha }}
|
||||||
|
image_digest: ${{ steps.build.outputs.digest }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install cosign
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract image metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||||
|
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "image_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./web/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }}
|
||||||
|
${{ steps.meta.outputs.image }}:latest
|
||||||
|
build-args: |
|
||||||
|
VITE_NETWORK=MAINNET
|
||||||
|
VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||||
|
VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||||
|
VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||||
|
VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||||
|
VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:cache
|
||||||
|
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:cache,mode=max
|
||||||
|
provenance: false
|
||||||
|
|
||||||
|
- name: Sign image with cosign (keyless, Sigstore Fulcio)
|
||||||
|
env:
|
||||||
|
COSIGN_EXPERIMENTAL: '1'
|
||||||
|
run: |
|
||||||
|
IMAGE_DIGEST="${{ steps.meta.outputs.image }}@${{ steps.build.outputs.digest }}"
|
||||||
|
# cosign needs its own registry auth — docker/login-action only writes
|
||||||
|
# ~/.docker/config.json which cosign on self-hosted runner can't read
|
||||||
|
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||||
|
echo "Signing $IMAGE_DIGEST"
|
||||||
|
cosign sign --yes "$IMAGE_DIGEST"
|
||||||
|
echo "✅ Image signed (transparency log: rekor.sigstore.dev)"
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# TELEGRAM CEO APPROVAL GATE
|
||||||
|
# Runs on self-hosted pwap-runner (DEV VPS) where pexsec-bot.service
|
||||||
|
# writes the gate file to /tmp/pexsec-gates/<sha> when CEO clicks
|
||||||
|
# Approve/Cancel in Telegram. 30-minute timeout = deploy cancelled.
|
||||||
|
# ========================================
|
||||||
|
telegram-gate:
|
||||||
|
name: Telegram deploy approval
|
||||||
|
runs-on: pwap-runner
|
||||||
|
needs: [web, security-audit]
|
||||||
|
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Send approval request and wait
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
|
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
MESSAGE: ${{ github.event.head_commit.message }}
|
||||||
|
run: |
|
||||||
|
SHORT="${SHA:0:7}"
|
||||||
|
GATE_DIR="/tmp/pexsec-gates"
|
||||||
|
mkdir -p "$GATE_DIR" 2>/dev/null || true
|
||||||
|
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Strip Markdown special chars to prevent Telegram parse errors
|
||||||
|
SAFE_MSG=$(echo "${MESSAGE}" | head -1 | tr -d '_*`[]()#|{}!' | cut -c1-120)
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"chat_id\": \"${CEO_CHAT_ID}\",
|
||||||
|
\"parse_mode\": \"Markdown\",
|
||||||
|
\"text\": \"🚀 *pwap/web Deploy Approval*\\n\\n\`${SHORT}\` — ${ACTOR}\\n\\n_${SAFE_MSG}_\\n\\nTargets: app.pezkuwichain.io + pex.mom\",
|
||||||
|
\"reply_markup\": {
|
||||||
|
\"inline_keyboard\": [[
|
||||||
|
{\"text\": \"✅ Approve\", \"callback_data\": \"deploy_approve:${SHORT}\"},
|
||||||
|
{\"text\": \"❌ Cancel\", \"callback_data\": \"deploy_cancel:${SHORT}\"}
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
|
||||||
|
echo "Waiting for Telegram approval (max 30 min)..."
|
||||||
|
TIMEOUT=1800
|
||||||
|
ELAPSED=0
|
||||||
|
while [ $ELAPSED -lt $TIMEOUT ]; do
|
||||||
|
if [ -f "$GATE_DIR/$SHORT" ]; then
|
||||||
|
DECISION=$(cat "$GATE_DIR/$SHORT")
|
||||||
|
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||||
|
if [ "$DECISION" = "approved" ]; then
|
||||||
|
echo "Deploy approved."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Deploy cancelled."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
ELAPSED=$((ELAPSED + 10))
|
||||||
|
done
|
||||||
|
echo "No approval received within 30 minutes — deploy cancelled."
|
||||||
|
exit 1
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# VERSION BUMP (RUNS BEFORE BOTH DEPLOYS)
|
# VERSION BUMP (RUNS BEFORE BOTH DEPLOYS)
|
||||||
# ========================================
|
# ========================================
|
||||||
bump-version:
|
bump-version:
|
||||||
name: Bump Version
|
name: Bump Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web, security-audit]
|
needs: [web, security-audit, telegram-gate, build-image]
|
||||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
# Skip on rollback (workflow_dispatch with rollback_to set)
|
||||||
|
if: |
|
||||||
|
github.ref == 'refs/heads/main' &&
|
||||||
|
(github.event_name == 'push' ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||||
outputs:
|
outputs:
|
||||||
new_version: ${{ steps.bump.outputs.version }}
|
new_version: ${{ steps.bump.outputs.version }}
|
||||||
|
|
||||||
@@ -116,19 +276,80 @@ jobs:
|
|||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# DEPLOY TO app.pezkuwichain.io (DEV VPS)
|
# DEPLOY TO app.pezkuwichain.io (DEV VPS)
|
||||||
|
# Pulls SHA-tagged image from GHCR, extracts /dist, scp to VPS.
|
||||||
|
# Health check + auto-rollback to .deploy-tag-prev on failure.
|
||||||
# ========================================
|
# ========================================
|
||||||
deploy-app:
|
deploy-app:
|
||||||
name: Deploy app.pezkuwichain.io
|
name: Deploy app.pezkuwichain.io
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [bump-version]
|
needs: [telegram-gate, bump-version, build-image]
|
||||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
if: |
|
||||||
|
always() &&
|
||||||
|
needs.telegram-gate.result == 'success' &&
|
||||||
|
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
env:
|
||||||
|
DOMAIN: app.pezkuwichain.io
|
||||||
|
TARGET_PATH: /var/www/subdomains/app
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download build artifact
|
- name: Determine image SHA
|
||||||
uses: actions/download-artifact@v4
|
id: sha
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||||
|
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Rolling back to: ${{ github.event.inputs.rollback_to }}"
|
||||||
|
else
|
||||||
|
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Capture currently-live SHA (for auto-rollback)
|
||||||
|
id: prev
|
||||||
|
run: |
|
||||||
|
# /.deploy-sha is written into every deploy; read what's live now
|
||||||
|
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||||
|
echo "Previous live SHA: ${PREV:-unknown}"
|
||||||
|
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
name: web-dist
|
registry: ${{ env.REGISTRY }}
|
||||||
path: dist/
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install cosign (for verify)
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
|
- name: Verify image signature (cosign keyless)
|
||||||
|
env:
|
||||||
|
COSIGN_EXPERIMENTAL: '1'
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||||
|
echo "Verifying signature for $IMAGE"
|
||||||
|
cosign verify "$IMAGE" \
|
||||||
|
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||||
|
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||||
|
> /dev/null
|
||||||
|
echo "✅ Signature valid — image was built by trusted CI"
|
||||||
|
|
||||||
|
- name: Extract /dist from image
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
mkdir -p dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
# Stamp this build's SHA into dist so future deploys can read PREV
|
||||||
|
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||||
|
ls -la dist/ | head -10
|
||||||
|
|
||||||
- name: Deploy to DEV VPS
|
- name: Deploy to DEV VPS
|
||||||
uses: appleboy/scp-action@v1.0.0
|
uses: appleboy/scp-action@v1.0.0
|
||||||
@@ -141,25 +362,158 @@ jobs:
|
|||||||
target: '/var/www/subdomains/app'
|
target: '/var/www/subdomains/app'
|
||||||
strip_components: 1
|
strip_components: 1
|
||||||
|
|
||||||
- name: Post-deploy notification
|
- name: Health check (60s window)
|
||||||
|
id: healthcheck
|
||||||
run: |
|
run: |
|
||||||
echo "✅ Deployed v${{ needs.bump-version.outputs.new_version }} to app.pezkuwichain.io"
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# ── Automatic rollback: pull PREV SHA image, redeploy, recheck ──
|
||||||
|
- name: Auto-rollback to previous SHA
|
||||||
|
id: rollback
|
||||||
|
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||||
|
run: |
|
||||||
|
PREV="${{ steps.prev.outputs.prev }}"
|
||||||
|
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
rm -rf dist && mkdir dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
echo "$PREV" > dist/.deploy-sha
|
||||||
|
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: SCP rollback artifact
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
uses: appleboy/scp-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_SSH_PORT || 2222 }}
|
||||||
|
source: 'dist/*'
|
||||||
|
target: '/var/www/subdomains/app'
|
||||||
|
strip_components: 1
|
||||||
|
|
||||||
|
- name: Re-health-check after rollback
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
id: healthcheck_rb
|
||||||
|
run: |
|
||||||
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Rollback also failed!"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Post-deploy notification
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||||
|
|
||||||
|
- name: Notify failure (Telegram)
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
|
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||||
|
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||||
|
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||||
|
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||||
|
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||||
|
run: |
|
||||||
|
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||||
|
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||||
|
elif [ -z "$PREV_SHA" ]; then
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||||
|
else
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||||
|
fi
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
|
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# DEPLOY TO pex.mom (VPS3 — geo-redundant mirror)
|
# DEPLOY TO pex.mom (VPS3 — geo-redundant mirror)
|
||||||
# ========================================
|
# ========================================
|
||||||
deploy-pex:
|
deploy-pex:
|
||||||
name: Deploy pex.mom
|
name: Deploy pex.mom
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [bump-version]
|
needs: [telegram-gate, bump-version, build-image]
|
||||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
if: |
|
||||||
|
always() &&
|
||||||
|
needs.telegram-gate.result == 'success' &&
|
||||||
|
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
env:
|
||||||
|
DOMAIN: pex.mom
|
||||||
|
TARGET_PATH: /var/www/pex.mom
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download build artifact
|
- name: Determine image SHA
|
||||||
uses: actions/download-artifact@v4
|
id: sha
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||||
|
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Capture currently-live SHA (for auto-rollback)
|
||||||
|
id: prev
|
||||||
|
run: |
|
||||||
|
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||||
|
echo "Previous live SHA: ${PREV:-unknown}"
|
||||||
|
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
name: web-dist
|
registry: ${{ env.REGISTRY }}
|
||||||
path: dist/
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install cosign (for verify)
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
|
- name: Verify image signature (cosign keyless)
|
||||||
|
env:
|
||||||
|
COSIGN_EXPERIMENTAL: '1'
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
echo "Verifying signature for $IMAGE"
|
||||||
|
cosign verify "$IMAGE" \
|
||||||
|
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||||
|
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||||
|
> /dev/null
|
||||||
|
echo "✅ Signature valid — image was built by trusted CI"
|
||||||
|
|
||||||
|
- name: Extract /dist from image
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
mkdir -p dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||||
|
|
||||||
- name: Deploy to VPS3
|
- name: Deploy to VPS3
|
||||||
uses: appleboy/scp-action@v1.0.0
|
uses: appleboy/scp-action@v1.0.0
|
||||||
@@ -172,35 +526,153 @@ jobs:
|
|||||||
target: '/var/www/pex.mom'
|
target: '/var/www/pex.mom'
|
||||||
strip_components: 1
|
strip_components: 1
|
||||||
|
|
||||||
- name: Post-deploy notification
|
- name: Health check (60s window)
|
||||||
|
id: healthcheck
|
||||||
run: |
|
run: |
|
||||||
echo "✅ Deployed v${{ needs.bump-version.outputs.new_version }} to pex.mom"
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Auto-rollback to previous SHA
|
||||||
|
id: rollback
|
||||||
|
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||||
|
run: |
|
||||||
|
PREV="${{ steps.prev.outputs.prev }}"
|
||||||
|
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
rm -rf dist && mkdir dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
echo "$PREV" > dist/.deploy-sha
|
||||||
|
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: SCP rollback artifact
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
uses: appleboy/scp-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_PEX_HOST }}
|
||||||
|
username: ${{ secrets.VPS_PEX_USER }}
|
||||||
|
key: ${{ secrets.VPS_PEX_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
|
||||||
|
source: 'dist/*'
|
||||||
|
target: '/var/www/pex.mom'
|
||||||
|
strip_components: 1
|
||||||
|
|
||||||
|
- name: Re-health-check after rollback
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
id: healthcheck_rb
|
||||||
|
run: |
|
||||||
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Rollback also failed!"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Post-deploy notification
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||||
|
|
||||||
|
- name: Notify failure (Telegram)
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
|
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||||
|
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||||
|
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||||
|
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||||
|
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||||
|
run: |
|
||||||
|
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||||
|
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||||
|
elif [ -z "$PREV_SHA" ]; then
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||||
|
else
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||||
|
fi
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
|
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# SECURITY CHECKS (BLOCKING)
|
# SECURITY CHECKS (BLOCKING)
|
||||||
|
# npm audit (high + critical) + TruffleHog secret scan
|
||||||
# ========================================
|
# ========================================
|
||||||
security-audit:
|
security-audit:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web]
|
needs: [web]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Web - npm audit (critical only)
|
- name: Web — npm audit (high + critical, production deps only)
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm audit --audit-level=critical
|
# Audit only production dependencies. Build tooling (vite, esbuild,
|
||||||
|
# vite-plugin-node-polyfills → elliptic, etc.) ships to no user, and
|
||||||
|
# advisories on those dev deps kept blocking production deploys.
|
||||||
|
npm audit --audit-level=high --omit=dev
|
||||||
|
|
||||||
- name: TruffleHog Secret Scan
|
- name: TruffleHog — PR diff (verified secrets only)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: trufflesecurity/trufflehog@main
|
||||||
|
with:
|
||||||
|
base: ${{ github.event.pull_request.base.sha }}
|
||||||
|
head: ${{ github.event.pull_request.head.sha }}
|
||||||
|
extra_args: --only-verified
|
||||||
|
|
||||||
|
- name: TruffleHog — full repo scan (verified secrets only)
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
uses: trufflesecurity/trufflehog@main
|
uses: trufflesecurity/trufflehog@main
|
||||||
with:
|
with:
|
||||||
path: ./
|
path: ./
|
||||||
extra_args: --only-verified
|
extra_args: --only-verified
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# CI GATE — explicit merge-block
|
||||||
|
# All required checks must succeed (or be skipped, e.g. for rollback path).
|
||||||
|
# Branch protection on main should require this job's success.
|
||||||
|
# ========================================
|
||||||
|
ci-gate:
|
||||||
|
name: CI Gate ✅
|
||||||
|
runs-on: pwap-runner
|
||||||
|
needs: [web, security-audit]
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Verify all required jobs succeeded or were intentionally skipped
|
||||||
|
run: |
|
||||||
|
results='${{ toJSON(needs) }}'
|
||||||
|
echo "$results" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
needs = json.load(sys.stdin)
|
||||||
|
failed = [name for name, job in needs.items() if job['result'] not in ('success', 'skipped')]
|
||||||
|
if failed:
|
||||||
|
print('❌ Required jobs failed: ' + ', '.join(failed))
|
||||||
|
sys.exit(1)
|
||||||
|
print('✅ All required CI jobs passed or skipped')
|
||||||
|
"
|
||||||
|
|||||||
-1
Submodule exchange deleted from bb3bc812ed
@@ -0,0 +1,53 @@
|
|||||||
|
# pwap/web — Static SPA build for distribution.
|
||||||
|
# Build context is the pwap repo ROOT (not web/) because vite aliases like
|
||||||
|
# @pezkuwi/utils, @shared/* resolve to ../shared/* — both web/ and shared/
|
||||||
|
# must be in the build context.
|
||||||
|
# Stage 1: build with Node. Stage 2: pure dist/ in busybox (smallest possible
|
||||||
|
# attacker surface — no shell, no package manager, no node runtime).
|
||||||
|
# Tag the resulting image with the git SHA in CI so rollback is just
|
||||||
|
# "pull pwap-web:<old-sha>".
|
||||||
|
|
||||||
|
# ─── Stage 1: Build ────────────────────────────────────────────
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /build/web
|
||||||
|
|
||||||
|
# Copy package files first to leverage Docker layer cache when only src changes
|
||||||
|
COPY web/package.json web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy shared/ first (less frequently changed), then web/ source
|
||||||
|
COPY shared/ /build/shared/
|
||||||
|
COPY web/ /build/web/
|
||||||
|
|
||||||
|
# Build args for environment-specific values (passed from CI)
|
||||||
|
ARG VITE_NETWORK=MAINNET
|
||||||
|
ARG VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||||
|
ARG VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||||
|
ARG VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||||
|
ARG VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||||
|
ARG VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||||
|
ARG VITE_SUPABASE_URL
|
||||||
|
ARG VITE_SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
ENV VITE_NETWORK=$VITE_NETWORK
|
||||||
|
ENV VITE_WS_ENDPOINT=$VITE_WS_ENDPOINT
|
||||||
|
ENV VITE_WS_ENDPOINT_FALLBACK_1=$VITE_WS_ENDPOINT_FALLBACK_1
|
||||||
|
ENV VITE_ASSET_HUB_ENDPOINT=$VITE_ASSET_HUB_ENDPOINT
|
||||||
|
ENV VITE_PEOPLE_CHAIN_ENDPOINT=$VITE_PEOPLE_CHAIN_ENDPOINT
|
||||||
|
ENV VITE_WALLETCONNECT_PROJECT_ID=$VITE_WALLETCONNECT_PROJECT_ID
|
||||||
|
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
|
||||||
|
ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ─── Stage 2: Distribution image ───────────────────────────────
|
||||||
|
# busybox:musl gives us a tiny base (~1.5MB) with a shell for `cp` operations
|
||||||
|
# during deploy extraction, but no npm/curl/wget/ssh — minimal attack surface
|
||||||
|
# if the image were ever exposed.
|
||||||
|
FROM busybox:musl
|
||||||
|
WORKDIR /dist
|
||||||
|
COPY --from=builder /build/web/dist /dist
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/pezkuwichain/pwap"
|
||||||
|
LABEL org.opencontainers.image.description="pwap/web static SPA — Pezkuwi wallet/exchange frontend"
|
||||||
|
LABEL org.opencontainers.image.licenses="proprietary"
|
||||||
|
CMD ["sh", "-c", "echo 'pwap-web image — extract /dist via: docker create + docker cp'; sleep infinity"]
|
||||||
Generated
+119
-90
@@ -109,6 +109,7 @@
|
|||||||
"typescript-eslint": "^8.0.1",
|
"typescript-eslint": "^8.0.1",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-node-polyfills": "^0.25.0",
|
"vite-plugin-node-polyfills": "^0.25.0",
|
||||||
|
"vite-plugin-subresource-integrity": "^0.0.12",
|
||||||
"vitest": "^4.0.10"
|
"vitest": "^4.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3507,9 +3508,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.3",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
|
||||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
"integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -5070,31 +5071,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
|
||||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
"integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.1.0",
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/spy": "4.0.18",
|
"@vitest/spy": "4.1.8",
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.1.8",
|
||||||
"chai": "^6.2.1",
|
"chai": "^6.2.2",
|
||||||
"tinyrainbow": "^3.0.3"
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
|
||||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
"integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "4.0.18",
|
"@vitest/spy": "4.1.8",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.21"
|
"magic-string": "^0.30.21"
|
||||||
},
|
},
|
||||||
@@ -5103,7 +5104,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"msw": "^2.4.9",
|
"msw": "^2.4.9",
|
||||||
"vite": "^6.0.0 || ^7.0.0-0"
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"msw": {
|
"msw": {
|
||||||
@@ -5125,26 +5126,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
|
||||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tinyrainbow": "^3.0.3"
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
|
||||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
"integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.1.8",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -5152,13 +5153,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
|
||||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
"integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.1.8",
|
||||||
|
"@vitest/utils": "4.1.8",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -5167,9 +5169,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
|
||||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
"integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -5177,14 +5179,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
|
||||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.1.8",
|
||||||
"tinyrainbow": "^3.0.3"
|
"convert-source-map": "^2.0.0",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
@@ -5454,9 +5457,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": {
|
"node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": {
|
||||||
"version": "7.5.10",
|
"version": "7.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz",
|
||||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
"integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.3.0"
|
"node": ">=8.3.0"
|
||||||
@@ -6567,13 +6570,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserify-sign": {
|
"node_modules/browserify-sign": {
|
||||||
"version": "4.2.5",
|
"version": "4.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.6.tgz",
|
||||||
"integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==",
|
"integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bn.js": "^5.2.2",
|
"bn.js": "^5.2.3",
|
||||||
"browserify-rsa": "^4.1.1",
|
"browserify-rsa": "^4.1.1",
|
||||||
"create-hash": "^1.2.0",
|
"create-hash": "^1.2.0",
|
||||||
"create-hmac": "^1.1.7",
|
"create-hmac": "^1.1.7",
|
||||||
@@ -7028,6 +7031,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cookie-es": {
|
"node_modules/cookie-es": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
|
||||||
@@ -7641,9 +7651,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz",
|
||||||
"integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
|
"integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==",
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@types/trusted-types": "^2.0.7"
|
"@types/trusted-types": "^2.0.7"
|
||||||
@@ -7859,9 +7869,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -11147,9 +11157,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.1",
|
"version": "6.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||||
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11367,12 +11377,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.30.3",
|
"version": "6.30.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
|
||||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
"integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.2"
|
"@remix-run/router": "1.23.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -11382,13 +11392,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "6.30.3",
|
"version": "6.30.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
|
||||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
"integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.2",
|
"@remix-run/router": "1.23.3",
|
||||||
"react-router": "6.30.3"
|
"react-router": "6.30.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -12237,9 +12247,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/std-env": {
|
"node_modules/std-env": {
|
||||||
"version": "3.10.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
||||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -12696,9 +12706,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyrainbow": {
|
"node_modules/tinyrainbow": {
|
||||||
"version": "3.0.3",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -13259,6 +13269,13 @@
|
|||||||
"vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-subresource-integrity": {
|
||||||
|
"version": "0.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-subresource-integrity/-/vite-plugin-subresource-integrity-0.0.12.tgz",
|
||||||
|
"integrity": "sha512-geKEo1KgGA56G8CciaoKA3Yf7ckpR23zSuSW802xrisW6vnH+dAYjKXZygEcmFKfsOe0+r3uG7Oz9eFEwhdjxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite/node_modules/fdir": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -13291,31 +13308,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
|
||||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.0.18",
|
"@vitest/expect": "4.1.8",
|
||||||
"@vitest/mocker": "4.0.18",
|
"@vitest/mocker": "4.1.8",
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.1.8",
|
||||||
"@vitest/runner": "4.0.18",
|
"@vitest/runner": "4.1.8",
|
||||||
"@vitest/snapshot": "4.0.18",
|
"@vitest/snapshot": "4.1.8",
|
||||||
"@vitest/spy": "4.0.18",
|
"@vitest/spy": "4.1.8",
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.1.8",
|
||||||
"es-module-lexer": "^1.7.0",
|
"es-module-lexer": "^2.0.0",
|
||||||
"expect-type": "^1.2.2",
|
"expect-type": "^1.3.0",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"obug": "^2.1.1",
|
"obug": "^2.1.1",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"std-env": "^3.10.0",
|
"std-env": "^4.0.0-rc.1",
|
||||||
"tinybench": "^2.9.0",
|
"tinybench": "^2.9.0",
|
||||||
"tinyexec": "^1.0.2",
|
"tinyexec": "^1.0.2",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"tinyrainbow": "^3.0.3",
|
"tinyrainbow": "^3.1.0",
|
||||||
"vite": "^6.0.0 || ^7.0.0",
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
"why-is-node-running": "^2.3.0"
|
"why-is-node-running": "^2.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -13331,12 +13348,15 @@
|
|||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
"@vitest/browser-playwright": "4.0.18",
|
"@vitest/browser-playwright": "4.1.8",
|
||||||
"@vitest/browser-preview": "4.0.18",
|
"@vitest/browser-preview": "4.1.8",
|
||||||
"@vitest/browser-webdriverio": "4.0.18",
|
"@vitest/browser-webdriverio": "4.1.8",
|
||||||
"@vitest/ui": "4.0.18",
|
"@vitest/coverage-istanbul": "4.1.8",
|
||||||
|
"@vitest/coverage-v8": "4.1.8",
|
||||||
|
"@vitest/ui": "4.1.8",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@edge-runtime/vm": {
|
"@edge-runtime/vm": {
|
||||||
@@ -13357,6 +13377,12 @@
|
|||||||
"@vitest/browser-webdriverio": {
|
"@vitest/browser-webdriverio": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"@vitest/ui": {
|
"@vitest/ui": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
@@ -13365,6 +13391,9 @@
|
|||||||
},
|
},
|
||||||
"jsdom": {
|
"jsdom": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -13606,9 +13635,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.19.0",
|
"version": "8.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
+3
-1
@@ -120,7 +120,8 @@
|
|||||||
"@pezkuwi/x-textdecoder": "^14.0.25",
|
"@pezkuwi/x-textdecoder": "^14.0.25",
|
||||||
"@pezkuwi/x-textencoder": "^14.0.25",
|
"@pezkuwi/x-textencoder": "^14.0.25",
|
||||||
"@pezkuwi/x-ws": "^14.0.25",
|
"@pezkuwi/x-ws": "^14.0.25",
|
||||||
"@pezkuwi/networks": "^14.0.25"
|
"@pezkuwi/networks": "^14.0.25",
|
||||||
|
"elliptic": "^6.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
@@ -147,6 +148,7 @@
|
|||||||
"typescript-eslint": "^8.0.1",
|
"typescript-eslint": "^8.0.1",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-node-polyfills": "^0.25.0",
|
"vite-plugin-node-polyfills": "^0.25.0",
|
||||||
|
"vite-plugin-subresource-integrity": "^0.0.12",
|
||||||
"vitest": "^4.0.10"
|
"vitest": "^4.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
<linearGradient id="fireGradient" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||||
|
<stop offset="0%" style="stop-color:#1B5E20;stop-opacity:1"></stop>
|
||||||
|
<stop offset="30%" style="stop-color:#FF6F00;stop-opacity:1"></stop>
|
||||||
|
<stop offset="60%" style="stop-color:#FFD600;stop-opacity:1"></stop>
|
||||||
|
<stop offset="100%" style="stop-color:#D32F2F;stop-opacity:1"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
|
||||||
|
<linearGradient id="innerFlame" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||||
|
<stop offset="0%" style="stop-color:#FFD600;stop-opacity:1"></stop>
|
||||||
|
<stop offset="50%" style="stop-color:#FF8F00;stop-opacity:1"></stop>
|
||||||
|
<stop offset="100%" style="stop-color:#FF5722;stop-opacity:1"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="8" result="coloredBlur"></feGaussianBlur>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"></feMergeNode>
|
||||||
|
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
|
||||||
|
<radialGradient id="sunGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" style="stop-color:#FFD600;stop-opacity:0.6"></stop>
|
||||||
|
<stop offset="100%" style="stop-color:#FFD600;stop-opacity:0"></stop>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
|
||||||
|
<circle cx="256" cy="256" r="240" fill="#1a1a2e" opacity="0.9"></circle>
|
||||||
|
|
||||||
|
|
||||||
|
<circle cx="256" cy="280" r="180" fill="url(#sunGlow)" filter="url(#glow)"></circle>
|
||||||
|
|
||||||
|
|
||||||
|
<path d="M256 60
 C280 120 340 160 360 220
 C380 280 360 340 340 380
 C320 420 280 460 256 470
 C232 460 192 420 172 380
 C152 340 132 280 152 220
 C172 160 232 120 256 60Z" fill="url(#fireGradient)" filter="url(#glow)">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
|
||||||
|
|
||||||
|
<path d="M256 140
 C270 180 310 210 320 260
 C330 310 320 350 300 380
 C280 410 268 430 256 440
 C244 430 232 410 212 380
 C192 350 182 310 192 260
 C202 210 242 180 256 140Z" fill="url(#innerFlame)" opacity="0.95">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
|
||||||
|
|
||||||
|
<ellipse cx="256" cy="320" rx="50" ry="80" fill="#FFFDE7" opacity="0.8">
|
||||||
|
|
||||||
|
|
||||||
|
</ellipse>
|
||||||
|
|
||||||
|
|
||||||
|
<circle cx="200" cy="180" r="8" fill="#4CAF50" opacity="0.8">
|
||||||
|
|
||||||
|
|
||||||
|
</circle>
|
||||||
|
<circle cx="312" cy="200" r="6" fill="#4CAF50" opacity="0.7">
|
||||||
|
|
||||||
|
|
||||||
|
</circle>
|
||||||
|
<circle cx="230" cy="150" r="5" fill="#81C784" opacity="0.6">
|
||||||
|
|
||||||
|
</circle>
|
||||||
|
<circle cx="280" cy="165" r="7" fill="#66BB6A" opacity="0.7">
|
||||||
|
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||||
|
import { Pez20Badge } from './Pez20Badge';
|
||||||
import { AddTokenModal } from './AddTokenModal';
|
import { AddTokenModal } from './AddTokenModal';
|
||||||
import { TransferModal } from './TransferModal';
|
import { TransferModal } from './TransferModal';
|
||||||
import { XCMTeleportModal } from './XCMTeleportModal';
|
import { XCMTeleportModal } from './XCMTeleportModal';
|
||||||
@@ -811,6 +812,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
||||||
{t('balance.pezBalance')}
|
{t('balance.pezBalance')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<Pez20Badge className="flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -853,6 +855,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
<CardTitle className="text-lg font-medium text-gray-300">
|
<CardTitle className="text-lg font-medium text-gray-300">
|
||||||
{t('balance.usdtBalance')}
|
{t('balance.usdtBalance')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<Pez20Badge className="flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ const APP_SECTIONS: AppSection[] = [
|
|||||||
{ title: 'mobile.app.whatsKurd', icon: '💬', route: '/social/whatskurd' },
|
{ title: 'mobile.app.whatsKurd', icon: '💬', route: '/social/whatskurd' },
|
||||||
{ title: 'mobile.app.forum', icon: '📰', route: '/forum' },
|
{ title: 'mobile.app.forum', icon: '📰', route: '/forum' },
|
||||||
{ title: 'mobile.app.kurdMedia', icon: '📺', route: '/social/kurdmedia' },
|
{ title: 'mobile.app.kurdMedia', icon: '📺', route: '/social/kurdmedia' },
|
||||||
{ title: 'mobile.app.events', icon: '📅', route: '/forum', comingSoon: true },
|
{ title: 'mobile.app.events', icon: '📅', route: '/forum', href: 'https://kurdishtts.pezkiwi.app' },
|
||||||
{ title: 'mobile.app.help', icon: '❓', route: '/help' },
|
{ title: 'mobile.app.loto', icon: '🔥', imgIcon: '/loto-icon.svg', route: '/forum', href: 'https://loto.pex.mom' },
|
||||||
{ title: 'mobile.app.music', icon: '🎵', route: '/forum', comingSoon: true },
|
{ title: 'mobile.app.music', icon: '🎵', route: '/forum', comingSoon: true },
|
||||||
{ title: 'mobile.app.rewshenbir',icon: '📡', imgIcon: '/rewshenbir-icon.png', route: '/rewshenbir', href: 'https://rewshenbir.pezkuwi.app' },
|
{ title: 'mobile.app.rewshenbir',icon: '📡', imgIcon: '/rewshenbir-icon.png', route: '/rewshenbir', href: 'https://rewshenbir.pezkuwi.app' },
|
||||||
{ title: 'mobile.app.referral', icon: '👥', route: '/dashboard', requiresAuth: true },
|
{ title: 'mobile.app.referral', icon: '👥', route: '/dashboard', requiresAuth: true },
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ const APP_SECTIONS: AppSection[] = [
|
|||||||
{ title: 'mobile.app.whatsKurd', icon: '💬', route: '/social/whatskurd' },
|
{ title: 'mobile.app.whatsKurd', icon: '💬', route: '/social/whatskurd' },
|
||||||
{ title: 'mobile.app.forum', icon: '📰', route: '/forum' },
|
{ title: 'mobile.app.forum', icon: '📰', route: '/forum' },
|
||||||
{ title: 'mobile.app.kurdMedia', icon: '📺', route: '/social/kurdmedia' },
|
{ title: 'mobile.app.kurdMedia', icon: '📺', route: '/social/kurdmedia' },
|
||||||
{ title: 'mobile.app.events', icon: '📅', route: '/forum', comingSoon: true },
|
{ title: 'mobile.app.events', icon: '📅', route: '/forum', href: 'https://kurdishtts.pezkiwi.app' },
|
||||||
{ title: 'mobile.app.help', icon: '❓', route: '/help' },
|
{ title: 'mobile.app.loto', icon: '🔥', imgIcon: '/loto-icon.svg', route: '/forum', href: 'https://loto.pex.mom' },
|
||||||
{ title: 'mobile.app.music', icon: '🎵', route: '/forum', comingSoon: true },
|
{ title: 'mobile.app.music', icon: '🎵', route: '/forum', comingSoon: true },
|
||||||
{ title: 'mobile.app.rewshenbir', icon: '📡', imgIcon: '/rewshenbir-icon.png', route: '/rewshenbir', href: 'https://rewshenbir.pezkuwi.app' },
|
{ title: 'mobile.app.rewshenbir', icon: '📡', imgIcon: '/rewshenbir-icon.png', route: '/rewshenbir', href: 'https://rewshenbir.pezkuwi.app' },
|
||||||
{ title: 'mobile.app.referral', icon: '👥', route: '/dashboard', requiresAuth: true },
|
{ title: 'mobile.app.referral', icon: '👥', route: '/dashboard', requiresAuth: true },
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small pill marking a token as a Pezkuwi token-standard asset.
|
||||||
|
* PEZ-20 = fungible standard (pallet-assets on Asset Hub), PEZ-721 = NFT standard.
|
||||||
|
* See docs.pezkuwichain.io → Token Standards.
|
||||||
|
*/
|
||||||
|
export const Pez20Badge: React.FC<{ standard?: 'PEZ-20' | 'PEZ-721'; className?: string }> = ({
|
||||||
|
standard = 'PEZ-20',
|
||||||
|
className = '',
|
||||||
|
}) => (
|
||||||
|
<a
|
||||||
|
href="https://docs.pezkuwichain.io/token-standards"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={`${standard} token standard on Pezkuwi Asset Hub`}
|
||||||
|
className={
|
||||||
|
'inline-flex items-center rounded-full border border-blue-500/40 bg-blue-500/10 ' +
|
||||||
|
'px-2 py-0.5 text-[10px] font-semibold tracking-wide text-blue-300 ' +
|
||||||
|
'hover:bg-blue-500/20 transition-colors no-underline ' +
|
||||||
|
className
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{standard}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Pez20Badge;
|
||||||
@@ -15,6 +15,8 @@ interface ChainStats {
|
|||||||
validators: number;
|
validators: number;
|
||||||
nominators: number;
|
nominators: number;
|
||||||
collators: number;
|
collators: number;
|
||||||
|
collatorsAH: number;
|
||||||
|
collatorsPeople: number;
|
||||||
activeProposals: number;
|
activeProposals: number;
|
||||||
totalVoters: number;
|
totalVoters: number;
|
||||||
citizenCount: number;
|
citizenCount: number;
|
||||||
@@ -325,6 +327,7 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
const [stats, setStats] = useState<ChainStats>({
|
const [stats, setStats] = useState<ChainStats>({
|
||||||
latestBlock: 0, finalizedBlock: 0, blockHash: '',
|
latestBlock: 0, finalizedBlock: 0, blockHash: '',
|
||||||
peers: 0, validators: 0, nominators: 0, collators: 0,
|
peers: 0, validators: 0, nominators: 0, collators: 0,
|
||||||
|
collatorsAH: 0, collatorsPeople: 0,
|
||||||
activeProposals: 0, totalVoters: 0, citizenCount: 0,
|
activeProposals: 0, totalVoters: 0, citizenCount: 0,
|
||||||
tokensStakedPct: '—',
|
tokensStakedPct: '—',
|
||||||
});
|
});
|
||||||
@@ -417,12 +420,7 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
const validators = sessionVals.length;
|
const validators = sessionVals.length;
|
||||||
setStats(prev => ({ ...prev, activeProposals, totalVoters, validators }));
|
setStats(prev => ({ ...prev, activeProposals, totalVoters, validators }));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
// Nominators/staking migrated to Asset Hub — counted in the Asset Hub effect below.
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const nomCount = await (api.query.staking as any).counterForNominators?.();
|
|
||||||
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
|
||||||
} catch {}
|
|
||||||
})();
|
})();
|
||||||
}, [api, isApiReady]);
|
}, [api, isApiReady]);
|
||||||
|
|
||||||
@@ -448,10 +446,18 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// Nominators live on Asset Hub after the staking migration (AHM).
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const collCount = await (assetHubApi.query.collatorSelection as any)?.candidates?.();
|
const nomCount = await (assetHubApi.query.staking as any)?.counterForNominators?.();
|
||||||
if (collCount != null) setStats(prev => ({ ...prev, collators: collCount.length }));
|
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Collators are the invulnerable set (not staking candidates, which are empty).
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const inv = await (assetHubApi.query.collatorSelection as any)?.invulnerables?.();
|
||||||
|
if (inv != null) setStats(prev => ({ ...prev, collatorsAH: inv.length, collators: inv.length + prev.collatorsPeople }));
|
||||||
} catch {}
|
} catch {}
|
||||||
})();
|
})();
|
||||||
}, [assetHubApi, isAssetHubReady]);
|
}, [assetHubApi, isAssetHubReady]);
|
||||||
@@ -465,6 +471,13 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
const entries = await (peopleApi.query as any).tiki?.citizenNft?.entries?.();
|
const entries = await (peopleApi.query as any).tiki?.citizenNft?.entries?.();
|
||||||
if (entries) setStats(prev => ({ ...prev, citizenCount: entries.length }));
|
if (entries) setStats(prev => ({ ...prev, citizenCount: entries.length }));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// People Chain also runs invulnerable collators — add them to the total.
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const inv = await (peopleApi.query.collatorSelection as any)?.invulnerables?.();
|
||||||
|
if (inv != null) setStats(prev => ({ ...prev, collatorsPeople: inv.length, collators: prev.collatorsAH + inv.length }));
|
||||||
|
} catch {}
|
||||||
})();
|
})();
|
||||||
}, [peopleApi, isPeopleReady]);
|
}, [peopleApi, isPeopleReady]);
|
||||||
|
|
||||||
@@ -1086,7 +1099,7 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
<PalletItem icon="lp-i-chat" label={t('landing.pallets.whatskurd')} to="/social/whatskurd" requiresLogin />
|
<PalletItem icon="lp-i-chat" label={t('landing.pallets.whatskurd')} to="/social/whatskurd" requiresLogin />
|
||||||
<PalletItem icon="lp-i-forum" label={t('landing.pallets.forum')} to="/forum" />
|
<PalletItem icon="lp-i-forum" label={t('landing.pallets.forum')} to="/forum" />
|
||||||
<PalletItem icon="lp-i-media" label={t('landing.pallets.kurdmedia')} to="/social/kurdmedia" requiresLogin />
|
<PalletItem icon="lp-i-media" label={t('landing.pallets.kurdmedia')} to="/social/kurdmedia" requiresLogin />
|
||||||
<PalletItem icon="lp-i-cal" label={t('landing.pallets.events')} locked />
|
<PalletItem icon="lp-i-cal" label={t('landing.pallets.events')} external="https://kurdishtts.pezkiwi.app" />
|
||||||
<PalletItem icon="lp-i-help" label={t('landing.pallets.help')} to="/help" />
|
<PalletItem icon="lp-i-help" label={t('landing.pallets.help')} to="/help" />
|
||||||
<PalletItem icon="lp-i-music" label={t('landing.pallets.music')} locked />
|
<PalletItem icon="lp-i-music" label={t('landing.pallets.music')} locked />
|
||||||
<PalletItem imgSrc="/rewshenbir-icon.png" label={t('landing.pallets.rewshenbir')} external="https://rewshenbir.pezkuwi.app" />
|
<PalletItem imgSrc="/rewshenbir-icon.png" label={t('landing.pallets.rewshenbir')} external="https://rewshenbir.pezkuwi.app" />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { PlusCircle, Home, ClipboardList, TrendingUp, CheckCircle2, Clock, Store, Zap, Blocks, MessageSquare } from 'lucide-react';
|
import { PlusCircle, Home, ClipboardList, TrendingUp, CheckCircle2, Clock, Store, Zap, Blocks, MessageSquare, CreditCard } from 'lucide-react';
|
||||||
import { AdList } from './AdList';
|
import { AdList } from './AdList';
|
||||||
import { CreateAd } from './CreateAd';
|
import { CreateAd } from './CreateAd';
|
||||||
import { NotificationBell } from './NotificationBell';
|
import { NotificationBell } from './NotificationBell';
|
||||||
@@ -191,6 +191,15 @@ export function P2PDashboard() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://buy-sell.pezkiwi.app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="relative flex flex-col items-center gap-0.5 px-3 py-1.5 rounded-lg hover:bg-amber-900/30 transition-colors"
|
||||||
|
>
|
||||||
|
<CreditCard className="w-5 h-5 text-amber-400" />
|
||||||
|
<span className="text-[10px] text-amber-300">{t('p2pNav.buyVisa')}</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1561,6 +1561,7 @@ export default {
|
|||||||
'p2pNav.orders': 'الطلبات',
|
'p2pNav.orders': 'الطلبات',
|
||||||
'p2pNav.ads': 'الإعلانات',
|
'p2pNav.ads': 'الإعلانات',
|
||||||
'p2pNav.messages': 'الرسائل',
|
'p2pNav.messages': 'الرسائل',
|
||||||
|
'p2pNav.buyVisa': 'اشترِ بفيزا',
|
||||||
|
|
||||||
// P2P Messages Inbox
|
// P2P Messages Inbox
|
||||||
'p2pMessages.title': 'الرسائل',
|
'p2pMessages.title': 'الرسائل',
|
||||||
@@ -3787,7 +3788,7 @@ export default {
|
|||||||
'mobile.app.bank': 'البنك',
|
'mobile.app.bank': 'البنك',
|
||||||
'mobile.app.exchange': 'البورصة',
|
'mobile.app.exchange': 'البورصة',
|
||||||
'mobile.app.dex': 'Pez-DEX',
|
'mobile.app.dex': 'Pez-DEX',
|
||||||
'mobile.app.p2p': 'P2P',
|
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||||
'mobile.app.b2b': 'B2B',
|
'mobile.app.b2b': 'B2B',
|
||||||
'mobile.app.bacZekat': 'الضريبة/الزكاة',
|
'mobile.app.bacZekat': 'الضريبة/الزكاة',
|
||||||
'mobile.app.launchpad': 'منصة الإطلاق',
|
'mobile.app.launchpad': 'منصة الإطلاق',
|
||||||
@@ -3804,6 +3805,7 @@ export default {
|
|||||||
'mobile.app.kurdMedia': 'كورد ميديا',
|
'mobile.app.kurdMedia': 'كورد ميديا',
|
||||||
'mobile.app.events': 'الفعاليات',
|
'mobile.app.events': 'الفعاليات',
|
||||||
'mobile.app.help': 'المساعدة',
|
'mobile.app.help': 'المساعدة',
|
||||||
|
'mobile.app.loto': 'لوتو',
|
||||||
'mobile.app.music': 'الموسيقى',
|
'mobile.app.music': 'الموسيقى',
|
||||||
'mobile.app.vpn': 'VPN',
|
'mobile.app.vpn': 'VPN',
|
||||||
'mobile.app.referral': 'الإحالة',
|
'mobile.app.referral': 'الإحالة',
|
||||||
|
|||||||
@@ -1551,6 +1551,7 @@ export default {
|
|||||||
'p2pNav.orders': 'داواکاریەکان',
|
'p2pNav.orders': 'داواکاریەکان',
|
||||||
'p2pNav.ads': 'ڕیکلامەکان',
|
'p2pNav.ads': 'ڕیکلامەکان',
|
||||||
'p2pNav.messages': 'پەیامەکان',
|
'p2pNav.messages': 'پەیامەکان',
|
||||||
|
'p2pNav.buyVisa': 'بە ڤیزا بکڕە',
|
||||||
|
|
||||||
// P2P Messages Inbox
|
// P2P Messages Inbox
|
||||||
'p2pMessages.title': 'پەیامەکان',
|
'p2pMessages.title': 'پەیامەکان',
|
||||||
@@ -3777,7 +3778,7 @@ export default {
|
|||||||
'mobile.app.bank': 'بانک',
|
'mobile.app.bank': 'بانک',
|
||||||
'mobile.app.exchange': 'ئاڵوگۆڕ',
|
'mobile.app.exchange': 'ئاڵوگۆڕ',
|
||||||
'mobile.app.dex': 'Pez-DEX',
|
'mobile.app.dex': 'Pez-DEX',
|
||||||
'mobile.app.p2p': 'P2P',
|
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||||
'mobile.app.b2b': 'B2B',
|
'mobile.app.b2b': 'B2B',
|
||||||
'mobile.app.bacZekat': 'باج/زەکات',
|
'mobile.app.bacZekat': 'باج/زەکات',
|
||||||
'mobile.app.launchpad': 'دەستپێکردن',
|
'mobile.app.launchpad': 'دەستپێکردن',
|
||||||
@@ -3794,6 +3795,7 @@ export default {
|
|||||||
'mobile.app.kurdMedia': 'کوردمیدیا',
|
'mobile.app.kurdMedia': 'کوردمیدیا',
|
||||||
'mobile.app.events': 'چالاکی',
|
'mobile.app.events': 'چالاکی',
|
||||||
'mobile.app.help': 'یارمەتی',
|
'mobile.app.help': 'یارمەتی',
|
||||||
|
'mobile.app.loto': 'لۆتۆ',
|
||||||
'mobile.app.music': 'مۆسیقا',
|
'mobile.app.music': 'مۆسیقا',
|
||||||
'mobile.app.vpn': 'VPN',
|
'mobile.app.vpn': 'VPN',
|
||||||
'mobile.app.referral': 'ئاماژە',
|
'mobile.app.referral': 'ئاماژە',
|
||||||
|
|||||||
@@ -1915,6 +1915,7 @@ export default {
|
|||||||
'p2pNav.orders': 'Orders',
|
'p2pNav.orders': 'Orders',
|
||||||
'p2pNav.ads': 'Ads',
|
'p2pNav.ads': 'Ads',
|
||||||
'p2pNav.messages': 'Messages',
|
'p2pNav.messages': 'Messages',
|
||||||
|
'p2pNav.buyVisa': 'Buy with Visa',
|
||||||
|
|
||||||
// P2P Messages Inbox
|
// P2P Messages Inbox
|
||||||
'p2pMessages.title': 'Messages',
|
'p2pMessages.title': 'Messages',
|
||||||
@@ -3839,7 +3840,7 @@ export default {
|
|||||||
'mobile.app.bank': 'Bank',
|
'mobile.app.bank': 'Bank',
|
||||||
'mobile.app.exchange': 'Exchange',
|
'mobile.app.exchange': 'Exchange',
|
||||||
'mobile.app.dex': 'Pez-DEX',
|
'mobile.app.dex': 'Pez-DEX',
|
||||||
'mobile.app.p2p': 'P2P',
|
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||||
'mobile.app.b2b': 'B2B',
|
'mobile.app.b2b': 'B2B',
|
||||||
'mobile.app.bacZekat': 'Bac/Zekat',
|
'mobile.app.bacZekat': 'Bac/Zekat',
|
||||||
'mobile.app.launchpad': 'Launchpad',
|
'mobile.app.launchpad': 'Launchpad',
|
||||||
@@ -3856,6 +3857,7 @@ export default {
|
|||||||
'mobile.app.kurdMedia': 'KurdMedia',
|
'mobile.app.kurdMedia': 'KurdMedia',
|
||||||
'mobile.app.events': 'Events',
|
'mobile.app.events': 'Events',
|
||||||
'mobile.app.help': 'Help',
|
'mobile.app.help': 'Help',
|
||||||
|
'mobile.app.loto': 'Loto',
|
||||||
'mobile.app.music': 'Music',
|
'mobile.app.music': 'Music',
|
||||||
'mobile.app.vpn': 'VPN',
|
'mobile.app.vpn': 'VPN',
|
||||||
'mobile.app.rewshenbir': 'Rewshenbir',
|
'mobile.app.rewshenbir': 'Rewshenbir',
|
||||||
|
|||||||
@@ -1585,6 +1585,7 @@ export default {
|
|||||||
'p2pNav.orders': 'سفارشات',
|
'p2pNav.orders': 'سفارشات',
|
||||||
'p2pNav.ads': 'آگهیها',
|
'p2pNav.ads': 'آگهیها',
|
||||||
'p2pNav.messages': 'پیامها',
|
'p2pNav.messages': 'پیامها',
|
||||||
|
'p2pNav.buyVisa': 'خرید با ویزا',
|
||||||
|
|
||||||
// P2P Messages Inbox
|
// P2P Messages Inbox
|
||||||
'p2pMessages.title': 'پیامها',
|
'p2pMessages.title': 'پیامها',
|
||||||
@@ -3821,7 +3822,7 @@ export default {
|
|||||||
'mobile.app.bank': 'بانک',
|
'mobile.app.bank': 'بانک',
|
||||||
'mobile.app.exchange': 'صرافی',
|
'mobile.app.exchange': 'صرافی',
|
||||||
'mobile.app.dex': 'Pez-DEX',
|
'mobile.app.dex': 'Pez-DEX',
|
||||||
'mobile.app.p2p': 'P2P',
|
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||||
'mobile.app.b2b': 'B2B',
|
'mobile.app.b2b': 'B2B',
|
||||||
'mobile.app.bacZekat': 'مالیات/زکات',
|
'mobile.app.bacZekat': 'مالیات/زکات',
|
||||||
'mobile.app.launchpad': 'سکوی پرتاب',
|
'mobile.app.launchpad': 'سکوی پرتاب',
|
||||||
@@ -3838,6 +3839,7 @@ export default {
|
|||||||
'mobile.app.kurdMedia': 'کوردمدیا',
|
'mobile.app.kurdMedia': 'کوردمدیا',
|
||||||
'mobile.app.events': 'رویدادها',
|
'mobile.app.events': 'رویدادها',
|
||||||
'mobile.app.help': 'کمک',
|
'mobile.app.help': 'کمک',
|
||||||
|
'mobile.app.loto': 'لاتاری',
|
||||||
'mobile.app.music': 'موسیقی',
|
'mobile.app.music': 'موسیقی',
|
||||||
'mobile.app.vpn': 'VPN',
|
'mobile.app.vpn': 'VPN',
|
||||||
'mobile.app.referral': 'ارجاع',
|
'mobile.app.referral': 'ارجاع',
|
||||||
|
|||||||
@@ -1573,6 +1573,7 @@ export default {
|
|||||||
'p2pNav.orders': 'Ferman',
|
'p2pNav.orders': 'Ferman',
|
||||||
'p2pNav.ads': 'Reklam',
|
'p2pNav.ads': 'Reklam',
|
||||||
'p2pNav.messages': 'Peyam',
|
'p2pNav.messages': 'Peyam',
|
||||||
|
'p2pNav.buyVisa': 'Bi Visa bikire',
|
||||||
|
|
||||||
// P2P Messages Inbox
|
// P2P Messages Inbox
|
||||||
'p2pMessages.title': 'Peyam',
|
'p2pMessages.title': 'Peyam',
|
||||||
@@ -3804,7 +3805,7 @@ export default {
|
|||||||
'mobile.app.bank': 'Bank',
|
'mobile.app.bank': 'Bank',
|
||||||
'mobile.app.exchange': 'Danûstandin',
|
'mobile.app.exchange': 'Danûstandin',
|
||||||
'mobile.app.dex': 'Pez-DEX',
|
'mobile.app.dex': 'Pez-DEX',
|
||||||
'mobile.app.p2p': 'P2P',
|
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||||
'mobile.app.b2b': 'B2B',
|
'mobile.app.b2b': 'B2B',
|
||||||
'mobile.app.bacZekat': 'Bac/Zekat',
|
'mobile.app.bacZekat': 'Bac/Zekat',
|
||||||
'mobile.app.launchpad': 'Destpêk',
|
'mobile.app.launchpad': 'Destpêk',
|
||||||
@@ -3821,6 +3822,7 @@ export default {
|
|||||||
'mobile.app.kurdMedia': 'KurdMedya',
|
'mobile.app.kurdMedia': 'KurdMedya',
|
||||||
'mobile.app.events': 'Çalakî',
|
'mobile.app.events': 'Çalakî',
|
||||||
'mobile.app.help': 'Alîkarî',
|
'mobile.app.help': 'Alîkarî',
|
||||||
|
'mobile.app.loto': 'Loto',
|
||||||
'mobile.app.music': 'Muzîk',
|
'mobile.app.music': 'Muzîk',
|
||||||
'mobile.app.vpn': 'VPN',
|
'mobile.app.vpn': 'VPN',
|
||||||
'mobile.app.rewshenbir': 'Rewşenbir',
|
'mobile.app.rewshenbir': 'Rewşenbir',
|
||||||
|
|||||||
@@ -1567,6 +1567,7 @@ export default {
|
|||||||
'p2pNav.orders': 'Siparişler',
|
'p2pNav.orders': 'Siparişler',
|
||||||
'p2pNav.ads': 'İlanlar',
|
'p2pNav.ads': 'İlanlar',
|
||||||
'p2pNav.messages': 'Mesajlar',
|
'p2pNav.messages': 'Mesajlar',
|
||||||
|
'p2pNav.buyVisa': 'Visa ile Al',
|
||||||
|
|
||||||
// P2P Messages Inbox
|
// P2P Messages Inbox
|
||||||
'p2pMessages.title': 'Mesajlar',
|
'p2pMessages.title': 'Mesajlar',
|
||||||
@@ -3807,7 +3808,7 @@ export default {
|
|||||||
'mobile.app.bank': 'Banka',
|
'mobile.app.bank': 'Banka',
|
||||||
'mobile.app.exchange': 'Borsa',
|
'mobile.app.exchange': 'Borsa',
|
||||||
'mobile.app.dex': 'Pez-DEX',
|
'mobile.app.dex': 'Pez-DEX',
|
||||||
'mobile.app.p2p': 'P2P',
|
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||||
'mobile.app.b2b': 'B2B',
|
'mobile.app.b2b': 'B2B',
|
||||||
'mobile.app.bacZekat': 'Vergi/Zekat',
|
'mobile.app.bacZekat': 'Vergi/Zekat',
|
||||||
'mobile.app.launchpad': 'Launchpad',
|
'mobile.app.launchpad': 'Launchpad',
|
||||||
@@ -3824,6 +3825,7 @@ export default {
|
|||||||
'mobile.app.kurdMedia': 'KurdMedya',
|
'mobile.app.kurdMedia': 'KurdMedya',
|
||||||
'mobile.app.events': 'Etkinlikler',
|
'mobile.app.events': 'Etkinlikler',
|
||||||
'mobile.app.help': 'Yardım',
|
'mobile.app.help': 'Yardım',
|
||||||
|
'mobile.app.loto': 'Loto',
|
||||||
'mobile.app.music': 'Müzik',
|
'mobile.app.music': 'Müzik',
|
||||||
'mobile.app.vpn': 'VPN',
|
'mobile.app.vpn': 'VPN',
|
||||||
'mobile.app.rewshenbir': 'Rewshenbir',
|
'mobile.app.rewshenbir': 'Rewshenbir',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
@@ -12,7 +12,6 @@ const BEREKETLI_API = `${BEREKETLI_URL}/v1`;
|
|||||||
*/
|
*/
|
||||||
export default function Bereketli() {
|
export default function Bereketli() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -20,8 +19,10 @@ export default function Bereketli() {
|
|||||||
const {
|
const {
|
||||||
data: { session },
|
data: { session },
|
||||||
} = await supabase.auth.getSession();
|
} = await supabase.auth.getSession();
|
||||||
|
// Not signed in: skip SSO and send the user to the Bereketli site,
|
||||||
|
// which handles its own login. Never dead-end on this interstitial.
|
||||||
if (!session?.access_token) {
|
if (!session?.access_token) {
|
||||||
setError(t('bereketli.noSession', 'Lütfen önce giriş yapın'));
|
window.location.href = BEREKETLI_URL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,27 +47,14 @@ export default function Bereketli() {
|
|||||||
});
|
});
|
||||||
window.location.href = `${BEREKETLI_URL}/app?auth=${btoa(params.toString())}`;
|
window.location.href = `${BEREKETLI_URL}/app?auth=${btoa(params.toString())}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Bağlantı hatası');
|
// SSO failed (expired token, network, etc.) — fall back to the public
|
||||||
|
// Bereketli site instead of stranding the user on app.pezkuwichain.io.
|
||||||
|
if (import.meta.env.DEV) console.warn('Bereketli SSO failed, falling back:', err);
|
||||||
|
window.location.href = BEREKETLI_URL;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-6">
|
|
||||||
<div className="text-center space-y-4">
|
|
||||||
<p className="text-red-400 text-sm">{error}</p>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
className="inline-block px-4 py-2 bg-green-600 text-white rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
{t('common.backToHome', 'Ana Sayfaya Dön')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||||
<div className="text-center space-y-3">
|
<div className="text-center space-y-3">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface MediaChannel {
|
|||||||
descriptionKu: string;
|
descriptionKu: string;
|
||||||
description: string;
|
description: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SocialPlatform {
|
interface SocialPlatform {
|
||||||
@@ -21,7 +22,7 @@ interface SocialPlatform {
|
|||||||
|
|
||||||
const MEDIA_CHANNELS: MediaChannel[] = [
|
const MEDIA_CHANNELS: MediaChannel[] = [
|
||||||
{ id: 'dkstv', nameKu: 'DKS TV', name: 'DKS TV', icon: '📺', descriptionKu: 'Televizyona Dewleta Dijîtal a Kurdistanê', description: 'Digital Kurdistan State Television', color: '#E53935' },
|
{ id: 'dkstv', nameKu: 'DKS TV', name: 'DKS TV', icon: '📺', descriptionKu: 'Televizyona Dewleta Dijîtal a Kurdistanê', description: 'Digital Kurdistan State Television', color: '#E53935' },
|
||||||
{ id: 'dksgzt', nameKu: 'DKS Rojname', name: 'DKS Gazette', icon: '📰', descriptionKu: 'Nûçe û Daxuyaniyên Fermî', description: 'Official News & Announcements', color: '#1E88E5' },
|
{ id: 'dksgzt', nameKu: 'DKS Rojname', name: 'DKS Gazette', icon: '📰', descriptionKu: 'Nûçe û Daxuyaniyên Fermî', description: 'Official News & Announcements', color: '#1E88E5', url: 'https://news.pex.mom' },
|
||||||
{ id: 'dksradio', nameKu: 'DKS Radyo', name: 'DKS Radio', icon: '📻', descriptionKu: 'Radyoya Dewleta Dijîtal a Kurdistanê', description: 'Digital Kurdistan State Radio', color: '#7B1FA2' },
|
{ id: 'dksradio', nameKu: 'DKS Radyo', name: 'DKS Radio', icon: '📻', descriptionKu: 'Radyoya Dewleta Dijîtal a Kurdistanê', description: 'Digital Kurdistan State Radio', color: '#7B1FA2' },
|
||||||
{ id: 'dksmusic', nameKu: 'DKS Muzîk', name: 'DKS Music', icon: '🎵', descriptionKu: 'Weşana Muzîka Kurdî', description: 'Kurdish Music Streaming', color: '#00897B' },
|
{ id: 'dksmusic', nameKu: 'DKS Muzîk', name: 'DKS Music', icon: '🎵', descriptionKu: 'Weşana Muzîka Kurdî', description: 'Kurdish Music Streaming', color: '#00897B' },
|
||||||
{ id: 'dkspodcast',nameKu: 'DKS Podcast', name: 'DKS Podcast', icon: '🎙️', descriptionKu: 'Podcast û Gotûbêjên Kurdî', description: 'Kurdish Podcasts & Talks', color: '#F4511E' },
|
{ id: 'dkspodcast',nameKu: 'DKS Podcast', name: 'DKS Podcast', icon: '🎙️', descriptionKu: 'Podcast û Gotûbêjên Kurdî', description: 'Kurdish Podcasts & Talks', color: '#F4511E' },
|
||||||
@@ -71,8 +72,9 @@ export default function KurdMediaPage() {
|
|||||||
<p className="text-sm text-gray-300 mb-1">{t('kurdMedia.channels.desc', 'Weşanên fermî yên Dewleta Dijîtal a Kurdistanê.')}</p>
|
<p className="text-sm text-gray-300 mb-1">{t('kurdMedia.channels.desc', 'Weşanên fermî yên Dewleta Dijîtal a Kurdistanê.')}</p>
|
||||||
<p className="text-xs text-gray-500 mb-4">{t('kurdMedia.channels.descEn', 'Official broadcasts of Digital Kurdistan State. TV, radio, news and more.')}</p>
|
<p className="text-xs text-gray-500 mb-4">{t('kurdMedia.channels.descEn', 'Official broadcasts of Digital Kurdistan State. TV, radio, news and more.')}</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{MEDIA_CHANNELS.map(ch => (
|
{MEDIA_CHANNELS.map(ch => {
|
||||||
<div key={ch.id} className="flex items-center gap-3 bg-gray-800 rounded-xl p-3">
|
const inner = (
|
||||||
|
<>
|
||||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl flex-shrink-0" style={{ backgroundColor: ch.color }}>
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl flex-shrink-0" style={{ backgroundColor: ch.color }}>
|
||||||
{ch.icon}
|
{ch.icon}
|
||||||
</div>
|
</div>
|
||||||
@@ -80,11 +82,28 @@ export default function KurdMediaPage() {
|
|||||||
<p className="font-semibold text-white text-sm">{ch.nameKu}</p>
|
<p className="font-semibold text-white text-sm">{ch.nameKu}</p>
|
||||||
<p className="text-xs text-gray-400 truncate">{ch.descriptionKu}</p>
|
<p className="text-xs text-gray-400 truncate">{ch.descriptionKu}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{ch.url ? (
|
||||||
|
<span className="text-[10px] font-bold text-green-400 bg-green-400/10 px-2 py-1 rounded-full flex-shrink-0">
|
||||||
|
{t('kurdMedia.open', 'Open')} ↗
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span className="text-[10px] font-bold text-yellow-400 bg-yellow-400/10 px-2 py-1 rounded-full flex-shrink-0">
|
<span className="text-[10px] font-bold text-yellow-400 bg-yellow-400/10 px-2 py-1 rounded-full flex-shrink-0">
|
||||||
{t('kurdMedia.soon', 'Soon')}
|
{t('kurdMedia.soon', 'Soon')}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return ch.url ? (
|
||||||
|
<a key={ch.id} href={ch.url} target="_blank" rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-3 bg-gray-800 rounded-xl p-3 hover:bg-gray-700 transition-colors">
|
||||||
|
{inner}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div key={ch.id} className="flex items-center gap-3 bg-gray-800 rounded-xl p-3">
|
||||||
|
{inner}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||||
|
import subresourceIntegrity from 'vite-plugin-subresource-integrity';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ command }) => ({
|
export default defineConfig(({ command }) => ({
|
||||||
@@ -38,6 +39,10 @@ export default defineConfig(({ command }) => ({
|
|||||||
},
|
},
|
||||||
protocolImports: true,
|
protocolImports: true,
|
||||||
}),
|
}),
|
||||||
|
// SRI: production build sırasında <script>/<link> tag'lerine
|
||||||
|
// sha384 integrity hash ekle. CDN/proxy compromise olsa bile
|
||||||
|
// tampered asset browser tarafından load edilmez.
|
||||||
|
command === 'build' ? subresourceIntegrity({ algorithm: 'sha384' }) : null,
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
resolve: {
|
resolve: {
|
||||||
mainFields: ['browser', 'module', 'main', 'exports'],
|
mainFields: ['browser', 'module', 'main', 'exports'],
|
||||||
|
|||||||
Reference in New Issue
Block a user