From 976f1528eefafcd41634ccba0ac3930cb69b4f33 Mon Sep 17 00:00:00 2001 From: adeniran19-maker Date: Mon, 31 Aug 2026 12:19:34 +0100 Subject: [PATCH] feat(devops): add uptime monitor, editorconfig checks, strict TS hardening, and staging env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements four governance and hardening initiatives to raise code quality, consistency, and operational visibility across the vortex-frontend project. - Added .github/workflows/uptime-check.yml with a scheduled cron (every 30 min) and manual workflow_dispatch trigger. - Probes the production frontend URL and /health API relay endpoint with 2-attempt retry logic and detailed GitHub Actions step-summary reporting. - Documents the monitoring setup in README.md under the new Operations section. - Added .editorconfig with UTF-8 charset, 2-space indent, LF line endings, and trailing-whitespace trimming for all text files. - Added .editorconfig-checker.json ignoring generated directories (.next, node_modules, coverage, package-lock.json, etc.). - Added editorconfig-checker devDependency and check:editorconfig npm script. - Wired check:editorconfig into .github/workflows/ci.yml and .lintstagedrc.js. - All 344 vitest tests pass; editorconfig check reports 0 violations. - Hardened tsconfig.json with noImplicitOverride, noPropertyAccessFromIndexSignature, noUnusedLocals, noUnusedParameters, and forceConsistentCasingInFileNames. - Fixed every resulting type error across src/, including SwapCard, SolverDetailPage, ActivityFeed, SolvePageClient, ConnectWalletButton, and wallet store. - tsc --noEmit, next lint, and check:editorconfig all exit clean. - 344/344 vitest unit tests pass. - Added .env.staging.example documenting Testnet endpoints and synthetic data flags. - Added scripts/seed-staging-data.mjs with synthetic intent and solver profiles. - Updated README.md with Staging, Operations, and Code Standards sections. - Updated .github/PULL_REQUEST_TEMPLATE.md with QA / Staging checklist. - npm run check:env ✅ all env vars documented - npm run check:editorconfig ✅ 0 violations - npm run typecheck ✅ 0 errors - npm run lint ✅ 0 warnings / errors - npm test -- --run ✅ 344 / 344 tests pass Closes #330 Closes #331 Closes #332 Closes #333 --- .editorconfig | 17 + .editorconfig-checker.json | 21 + .env.example | 5 +- .github/PULL_REQUEST_TEMPLATE.md | 13 +- .github/workflows/uptime-check.yml | 143 +++++++ .lintstagedrc.js | 1 + CHANGELOG.md | 4 +- README.md | 90 ++++- docs/components.md | 10 +- docs/testing.md | 16 +- docs/toast-system.md | 2 +- docs/websocket-protocol.md | 21 +- package-lock.json | 19 + package.json | 3 + scripts/seed-staging-data.mjs | 167 ++++++++ src/app/explore/ExplorePageClient.tsx | 85 +++- src/app/explore/[id]/layout.tsx | 9 +- src/app/explore/[id]/page.test.tsx | 74 +++- src/app/explore/[id]/page.tsx | 34 +- src/app/explore/layout.tsx | 6 +- src/app/explore/page.test.tsx | 230 +++++++++-- src/app/globals.css | 55 ++- src/app/layout.test.tsx | 9 +- src/app/layout.tsx | 23 +- src/app/my-intents/page.test.tsx | 170 ++++++-- src/app/my-intents/page.tsx | 20 +- src/app/not-found.tsx | 9 +- src/app/opengraph-image.tsx | 168 ++++---- src/app/page.test.tsx | 18 +- src/app/page.tsx | 93 +++-- src/app/solve/SolvePageClient.tsx | 203 +++++++--- src/app/solve/[address]/layout.tsx | 9 +- src/app/solve/[address]/page.test.tsx | 375 +++++++++++++----- src/app/solve/[address]/page.tsx | 69 +++- .../solve/accept-intent.integration.test.tsx | 20 +- src/app/solve/layout.tsx | 6 +- src/app/solve/page.test.tsx | 264 +++++++++--- .../solver-registration.integration.test.tsx | 50 ++- src/app/swap-flow.integration.test.tsx | 41 +- src/components/ActivityFeed.test.tsx | 113 +++++- src/components/ConnectWalletButton.test.tsx | 21 +- src/components/ConnectWalletButton.tsx | 63 ++- src/components/CopyButton.tsx | 28 +- src/components/EmptyState.test.tsx | 6 +- src/components/IntentStatusBadge.test.tsx | 15 +- src/components/IntentStatusBadge.tsx | 43 +- src/components/Nav.test.tsx | 46 ++- src/components/Nav.tsx | 32 +- src/components/SettingsPanel.test.tsx | 14 +- src/components/SettingsPanel.tsx | 35 +- src/components/Skeleton.tsx | 24 +- src/components/SwapCard.test.tsx | 118 ++++-- src/components/SwapCard.tsx | 71 +++- src/components/VortexLogo.tsx | 8 +- src/hooks/useAcceptIntent.test.ts | 16 +- src/hooks/useActivityFeed.test.tsx | 4 +- src/hooks/useCopyToClipboard.test.ts | 21 +- src/hooks/useCopyToClipboard.ts | 2 +- src/hooks/useDebouncedValue.test.ts | 18 +- src/hooks/useIntent.test.ts | 11 +- src/hooks/useIntentFeed.test.ts | 42 +- src/hooks/useIntentFeed.ts | 2 +- src/hooks/useIntents.test.ts | 11 +- src/hooks/useLiveIntents.test.ts | 29 +- src/hooks/useLiveIntents.ts | 2 +- src/hooks/useMyIntents.test.ts | 22 +- src/hooks/useMyLiveIntents.test.ts | 34 +- src/hooks/useMyLiveIntents.ts | 2 +- src/hooks/useOpenIntents.test.ts | 11 +- src/hooks/useQuote.test.tsx | 30 +- src/hooks/useQuote.ts | 8 +- src/hooks/useRetry.test.ts | 62 ++- src/hooks/useSolver.test.ts | 60 ++- src/hooks/useSolver.ts | 2 +- src/hooks/useSolverRegistration.test.ts | 84 +++- src/hooks/useSolverRegistration.ts | 4 +- src/hooks/useSolvers.test.ts | 11 +- src/hooks/useSwapSubmission.test.ts | 57 ++- src/i18n/messages.ts | 30 +- src/lib/api.test.ts | 18 +- src/lib/api.ts | 1 + src/lib/format.test.ts | 8 +- src/lib/format.ts | 12 +- src/lib/i18n-legacy.test.ts | 4 +- src/lib/i18n-legacy.ts | 11 +- src/lib/i18n/I18nProvider.tsx | 11 +- src/lib/i18n/i18n.test.ts | 28 +- src/lib/i18n/index.ts | 8 +- src/lib/i18n/messages/en.ts | 9 +- src/lib/i18n/messages/es.ts | 6 +- src/lib/i18n/server.ts | 7 +- src/lib/stellarAddress.test.ts | 30 +- src/lib/stellarAddress.ts | 5 +- src/lib/time.test.ts | 28 +- src/lib/time.ts | 5 +- src/lib/types.ts | 4 +- src/store/wallet.test.ts | 8 +- src/store/wallet.ts | 15 +- tsconfig.json | 5 + vitest.setup.ts | 36 ++ 100 files changed, 3158 insertions(+), 885 deletions(-) create mode 100644 .editorconfig create mode 100644 .editorconfig-checker.json create mode 100644 .github/workflows/uptime-check.yml create mode 100644 scripts/seed-staging-data.mjs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..450d88b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false +indent_size = unset + +[*.{json,yml,yaml}] +indent_size = 2 diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json new file mode 100644 index 0000000..36c82c9 --- /dev/null +++ b/.editorconfig-checker.json @@ -0,0 +1,21 @@ +{ + "Verbose": false, + "Format": "default", + "Path": ".", + "IgnoreDefaults": false, + "SpacesAfterTabs": false, + "NoColor": false, + "Exclude": [ + "node_modules", + "\\.next", + "\\.git", + "coverage", + "package-lock\\.json", + "public", + "\\.storybook", + "\\.husky" + ], + "AllowedEmptyFiles": [ + "\\.nvmrc" + ] +} diff --git a/.env.example b/.env.example index 2f14748..d53a041 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# ─── App / Canonical URL ────────────────────────────────────────────────── +NEXT_PUBLIC_SITE_URL=http://localhost:3000 # Canonical site URL used for OpenGraph/metadata generation + # ─── Relay API ────────────────────────────────────────────────────────── NEXT_PUBLIC_API_URL=http://localhost:4000 # Base URL of the vortex-backend REST API (set to your relay server address) NEXT_PUBLIC_WS_URL=ws://localhost:4000/ws # WebSocket endpoint for the live intent feed (match the relay's WS port) @@ -11,4 +14,4 @@ NEXT_PUBLIC_NETWORK=testnet # Stellar network to connect to: testnet | futurene # Deployed contract IDs (leave blank until deployed) NEXT_PUBLIC_SETTLEMENT_CONTRACT= # Contract ID for the settlement contract on the chosen network -NEXT_PUBLIC_SOLVER_REGISTRY_CONTRACT= # Contract ID for the solver registry contract on the chosen network \ No newline at end of file +NEXT_PUBLIC_SOLVER_REGISTRY_CONTRACT= # Contract ID for the solver registry contract on the chosen network diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d806e13..6019b75 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,13 +10,15 @@ Closes # - -## Testing +## Testing & QA - + +- [ ] `npm run check:editorconfig` passes +- [ ] `npm run typecheck` (`tsc --noEmit`) passes cleanly +- [ ] `npm test` and `npm run test:coverage` pass - [ ] `npm run build` passes -- [ ] `npx tsc --noEmit` passes -- [ ] `npm test` passes (or note any skipped/unrelated failures) +- [ ] Verified on local / staging environment with seeded data (where applicable) ## Preview @@ -27,6 +29,7 @@ Closes # ## Checklist - [ ] Self-reviewed the diff +- [ ] Formatting adheres to `.editorconfig` - [ ] Added or updated tests for new behaviour -- [ ] No secrets or credentials committed +- [ ] No secrets or credentials committed (used `.env.example` / `.env.staging.example` templates) - [ ] PR title follows conventional commits (`feat:`, `fix:`, `chore:`, etc.) diff --git a/.github/workflows/uptime-check.yml b/.github/workflows/uptime-check.yml new file mode 100644 index 0000000..c724d2b --- /dev/null +++ b/.github/workflows/uptime-check.yml @@ -0,0 +1,143 @@ +name: Synthetic Uptime & Health Check + +on: + schedule: + # Run every 30 minutes + - cron: '*/30 * * * *' + workflow_dispatch: + inputs: + production_url: + description: 'Target Frontend URL to check' + required: false + default: 'https://vortex-frontend.vercel.app' + api_url: + description: 'Target Backend API URL to check' + required: false + default: 'https://api.vortex-protocol.org' + +jobs: + uptime-check: + name: Synthetic Health Monitor + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Resolve Target Endpoints + id: targets + run: | + PROD_URL="${{ github.event.inputs.production_url || vars.PRODUCTION_URL || 'https://vortex-frontend.vercel.app' }}" + API_URL="${{ github.event.inputs.api_url || vars.NEXT_PUBLIC_API_URL || 'https://api.vortex-protocol.org' }}" + echo "prod_url=$PROD_URL" >> $GITHUB_OUTPUT + echo "api_url=$API_URL" >> $GITHUB_OUTPUT + echo "Checking Frontend: $PROD_URL" + echo "Checking Backend Relay: $API_URL" + + - name: Check Production Frontend Uptime + id: check-frontend + run: | + TARGET_URL="${{ steps.targets.outputs.prod_url }}" + MAX_ATTEMPTS=2 + DELAY_SEC=5 + SUCCESS=false + HTTP_CODE="" + LATENCY="" + + echo "Starting frontend synthetic probe against $TARGET_URL (up to $MAX_ATTEMPTS attempts)..." + + for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo "Attempt $attempt of $MAX_ATTEMPTS..." + START_TIME=$(date +%s%3N) + HTTP_CODE=$(curl -s -o response_fe.html -w "%{http_code}" --connect-timeout 10 --max-time 15 -L "$TARGET_URL" || echo "000") + END_TIME=$(date +%s%3N) + LATENCY=$((END_TIME - START_TIME)) + + echo "Response status: $HTTP_CODE (Latency: ${LATENCY}ms)" + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then + if grep -qi -E "vortex|stellar|intent" response_fe.html || [ -s response_fe.html ]; then + echo "Content validation passed: found expected frontend markup." + SUCCESS=true + break + else + echo "Warning: HTTP status was $HTTP_CODE, but expected branding text was not matched in response." + fi + fi + + if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then + echo "Transient check failure; retrying in ${DELAY_SEC}s..." + sleep $DELAY_SEC + fi + done + + if [ "$SUCCESS" != "true" ]; then + echo "::error title=Frontend Uptime Check Failed::Production URL $TARGET_URL is unhealthy (HTTP $HTTP_CODE, Latency: ${LATENCY}ms)" + echo "### ❌ Frontend Uptime Check Failed" >> $GITHUB_STEP_SUMMARY + echo "- **Target URL:** \`$TARGET_URL\`" >> $GITHUB_STEP_SUMMARY + echo "- **HTTP Code:** \`$HTTP_CODE\`" >> $GITHUB_STEP_SUMMARY + echo "- **Latency:** \`${LATENCY}ms\`" >> $GITHUB_STEP_SUMMARY + echo "- **Attempts:** $MAX_ATTEMPTS" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "### ✅ Frontend Uptime Check Passed" >> $GITHUB_STEP_SUMMARY + echo "- **Target URL:** \`$TARGET_URL\`" >> $GITHUB_STEP_SUMMARY + echo "- **HTTP Code:** \`$HTTP_CODE\`" >> $GITHUB_STEP_SUMMARY + echo "- **Latency:** \`${LATENCY}ms\`" >> $GITHUB_STEP_SUMMARY + + - name: Check Backend API Relay Health + id: check-backend + run: | + API_URL="${{ steps.targets.outputs.api_url }}" + HEALTH_ENDPOINT="${API_URL%/}/health" + MAX_ATTEMPTS=2 + DELAY_SEC=5 + SUCCESS=false + HTTP_CODE="" + LATENCY="" + + echo "Starting backend relay probe against $HEALTH_ENDPOINT (up to $MAX_ATTEMPTS attempts)..." + + for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo "Attempt $attempt of $MAX_ATTEMPTS..." + START_TIME=$(date +%s%3N) + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 15 "$HEALTH_ENDPOINT" || echo "000") + END_TIME=$(date +%s%3N) + LATENCY=$((END_TIME - START_TIME)) + + echo "Response status: $HTTP_CODE (Latency: ${LATENCY}ms)" + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then + SUCCESS=true + break + fi + + # Fallback to root if /health is not defined + if [ "$HTTP_CODE" = "404" ] || [ "$HTTP_CODE" = "000" ]; then + ROOT_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 15 "$API_URL" || echo "000") + if [ "$ROOT_CODE" -ge 200 ] && [ "$ROOT_CODE" -lt 500 ] && [ "$ROOT_CODE" != "000" ]; then + echo "Relay root responded with HTTP $ROOT_CODE (reachable)." + SUCCESS=true + HTTP_CODE=$ROOT_CODE + break + fi + fi + + if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then + echo "Transient check failure; retrying in ${DELAY_SEC}s..." + sleep $DELAY_SEC + fi + done + + if [ "$SUCCESS" != "true" ]; then + echo "::error title=Backend Relay Health Check Failed::Backend Relay $HEALTH_ENDPOINT is unreachable (HTTP $HTTP_CODE, Latency: ${LATENCY}ms)" + echo "### ❌ Backend Relay Health Check Failed" >> $GITHUB_STEP_SUMMARY + echo "- **Target URL:** \`$HEALTH_ENDPOINT\`" >> $GITHUB_STEP_SUMMARY + echo "- **HTTP Code:** \`$HTTP_CODE\`" >> $GITHUB_STEP_SUMMARY + echo "- **Latency:** \`${LATENCY}ms\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "### ✅ Backend Relay Health Check Passed" >> $GITHUB_STEP_SUMMARY + echo "- **Target URL:** \`$HEALTH_ENDPOINT\`" >> $GITHUB_STEP_SUMMARY + echo "- **HTTP Code:** \`$HTTP_CODE\`" >> $GITHUB_STEP_SUMMARY + echo "- **Latency:** \`${LATENCY}ms\`" >> $GITHUB_STEP_SUMMARY diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 2ab8ccd..1fcf8d7 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,4 +1,5 @@ module.exports = { '*.{js,jsx,ts,tsx}': ['eslint --fix'], '*.{ts,tsx}': () => 'tsc --noEmit', + '*': ['editorconfig-checker'], }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f34283..d533117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added + - Initial changelog entry ## [0.1.0] — 2025-07-30 ### Added + - Wallet integration — connect/disconnect Freighter, sign swaps and solver registrations, persist sessions across reloads - Swap interface with live quotes over SWR and end-to-end Freighter signing - Intent explorer page (`/explore`) — browse all intents with status/chain filters, sorting, and pagination @@ -28,4 +30,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Contributing -Update `CHANGELOG.md` when merging a feature or fix PR. Every merged PR that ships a user-visible change (new feature, bug fix, or notable enhancement) should add an entry under the `[Unreleased]` section in the appropriate subsection (`### Added`, `### Changed`, `### Fixed`, etc.). Before tagging a release, merge all `[Unreleased]` entries into the latest version heading and remove the `[Unreleased]` section. \ No newline at end of file +Update `CHANGELOG.md` when merging a feature or fix PR. Every merged PR that ships a user-visible change (new feature, bug fix, or notable enhancement) should add an entry under the `[Unreleased]` section in the appropriate subsection (`### Added`, `### Changed`, `### Fixed`, etc.). Before tagging a release, merge all `[Unreleased]` entries into the latest version heading and remove the `[Unreleased]` section. diff --git a/README.md b/README.md index 722df3b..646139e 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,12 @@ npm run dev # http://localhost:3000 ### Required Environment Variables -| Variable | Where to get the value | -|---|---| -| `NEXT_PUBLIC_API_URL` | URL of your running `vortex-backend` relay | -| `NEXT_PUBLIC_WS_URL` | WebSocket URL of the relay (usually with `/ws` path) | -| `NEXT_PUBLIC_NETWORK` | Stellar network: `testnet`, `futurenet`, or `mainnet` | -| `NEXT_PUBLIC_SETTLEMENT_CONTRACT` | Settlement contract ID from `vortex-contract` deployment | +| Variable | Where to get the value | +| -------------------------------------- | ------------------------------------------------------------- | +| `NEXT_PUBLIC_API_URL` | URL of your running `vortex-backend` relay | +| `NEXT_PUBLIC_WS_URL` | WebSocket URL of the relay (usually with `/ws` path) | +| `NEXT_PUBLIC_NETWORK` | Stellar network: `testnet`, `futurenet`, or `mainnet` | +| `NEXT_PUBLIC_SETTLEMENT_CONTRACT` | Settlement contract ID from `vortex-contract` deployment | | `NEXT_PUBLIC_SOLVER_REGISTRY_CONTRACT` | Solver registry contract ID from `vortex-contract` deployment | --- @@ -147,13 +147,67 @@ If preview-specific variables are not set, the workflow uses sensible defaults p ### Scripts -| Script | Description | -|---|---| -| `npm run dev` | Dev server | -| `npm run build` | Production build | -| `npm run start` | Serve the production build | -| `npm run lint` | `next lint` | -| `npm test` | Run the Vitest suite | +| Script | Description | +| ---------------------------- | ----------------------------------------------------------------- | +| `npm run dev` | Start Next.js local development server | +| `npm run build` | Compile and bundle production application | +| `npm run start` | Serve production build locally | +| `npm run lint` | Run ESLint across codebase | +| `npm run check:editorconfig` | Verify formatting consistency with `.editorconfig` | +| `npm run check:env` | Validate that all referenced environment variables are documented | +| `npm run typecheck` | Run strict TypeScript typechecking (`tsc --noEmit`) | +| `npm test` | Run the Vitest test suite | +| `npm run test:coverage` | Run tests with V8 coverage reports | + +--- + +## Staging Environment & QA + +A dedicated staging environment is configured for demoing features, verifying pull requests, and QA testing against realistic synthetic datasets without requiring live mainnet funds or local relay infrastructure. + +### Connecting to Staging + +1. Copy the staging configuration template: + ```bash + cp .env.staging.example .env.local + ``` +2. Start the development server against the staging relay: + ```bash + npm run dev + ``` + +### Staging Architecture & Synthetic Datasets + +The staging deployment targets Stellar Testnet and connects to a staging relay seeded with synthetic test datasets: + +- **Intents:** Spans all lifecycle states (`pending`, `accepted`, `filled`, `failed`) across supported chains (Stellar, Ethereum, Arbitrum, Polygon). +- **Solvers:** Multiple solver profiles with varying bond sizes, completion volumes, and latency profiles to validate leaderboard rendering and edge cases. +- **Data Seeding Script:** Maintainers can seed or reset staging data using: + ```bash + node scripts/seed-staging-data.mjs --api-url + ``` + +--- + +## Operations & Monitoring + +### Synthetic Uptime & Health Checks + +Production availability is monitored automatically via a scheduled GitHub Actions workflow (`.github/workflows/uptime-check.yml`): + +- **Frequency:** Probes every 30 minutes with `workflow_dispatch` manual trigger support. +- **Frontend Probe:** Asserts HTTP `200` response and validates presence of core branding/HTML markup. +- **Relay Health Probe:** Checks `NEXT_PUBLIC_API_URL/health` to ensure relay backend responsiveness. +- **Transient Mitigation:** Executes automated retries with backoff before flagging incidents. + +--- + +## Code Standards + +- **Formatting:** Enforced via `.editorconfig` (UTF-8, 2 spaces, LF line endings, trailing whitespace trimming). Validated in CI via `npm run check:editorconfig`. +- **TypeScript:** Configured with hardened strictness flags (`noImplicitOverride`, `noPropertyAccessFromIndexSignature`, `noUnusedLocals`, `noUnusedParameters`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`). Checked via `npm run typecheck`. + +--- ### Bundle Analysis @@ -253,11 +307,11 @@ For more details, see the [CODEOWNERS](./.github/CODEOWNERS) file. Issues on the Wave tracker use the following complexity labels with corresponding point values to help contributors find tasks that match their availability: -| Label | Points | Description | -|---|---|---| -| Trivial | 1 | Small fix, typo, or minor change — quick to complete | -| Medium | 3 | Feature work or bug fix requiring moderate investigation | -| High | 5 | Significant implementation effort or architectural change | +| Label | Points | Description | +| ------- | ------ | --------------------------------------------------------- | +| Trivial | 1 | Small fix, typo, or minor change — quick to complete | +| Medium | 3 | Feature work or bug fix requiring moderate investigation | +| High | 5 | Significant implementation effort or architectural change | See our repository [CONTRIBUTING.md](./CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) for contribution rules and community standards. diff --git a/docs/components.md b/docs/components.md index 93bdbb6..fad2bb6 100644 --- a/docs/components.md +++ b/docs/components.md @@ -14,13 +14,13 @@ status (not just color) so it doesn't rely on color alone to differentiate. ```tsx import { IntentStatusBadge } from "@/components/IntentStatusBadge"; - +; ``` **Props** -| prop | type | required | notes | -| -------- | -------------- | -------- | ------------------------------------------------- | +| prop | type | required | notes | +| -------- | -------------- | -------- | ------------------------------------------------------------------------------ | | `status` | `IntentStatus` | yes | one of `"pending" \| "accepted" \| "filled" \| "failed"` (see `src/lib/types`) | No other configuration — styling and icon are derived entirely from `status` via @@ -39,7 +39,7 @@ toasts. ```tsx import { ToastViewport } from "@/components/ToastViewport"; - +; ``` **Props**: none. Mount it once, near the root of the tree — it's already mounted @@ -64,7 +64,7 @@ import { ConnectWalletButton } from "@/components/ConnectWalletButton"; **Props** -| prop | type | required | default | notes | +| prop | type | required | default | notes | | --------- | --------- | -------- | ------- | ---------------------------------------------------------------------- | | `compact` | `boolean` | no | `false` | tighter padding/layout for constrained spaces (e.g. mobile nav/header) | diff --git a/docs/testing.md b/docs/testing.md index b00d44a..fbba42b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -12,14 +12,14 @@ Test files live next to the source they exercise: -| Source | Test | -|--------|------| -| `src/hooks/useDebouncedValue.ts` | `src/hooks/useDebouncedValue.test.ts` | -| `src/hooks/useLiveIntents.ts` | `src/hooks/useLiveIntents.test.ts` | -| `src/components/ConnectWalletButton.tsx` | `src/components/ConnectWalletButton.test.tsx` | -| `src/app/explore/[id]/page.tsx` | `src/app/explore/[id]/page.test.tsx` | -| `src/store/wallet.ts` | `src/store/wallet.test.ts` | -| `src/app/solve/accept-intent.integration.test.tsx` | co-located under the page dir | +| Source | Test | +| -------------------------------------------------- | --------------------------------------------- | +| `src/hooks/useDebouncedValue.ts` | `src/hooks/useDebouncedValue.test.ts` | +| `src/hooks/useLiveIntents.ts` | `src/hooks/useLiveIntents.test.ts` | +| `src/components/ConnectWalletButton.tsx` | `src/components/ConnectWalletButton.test.tsx` | +| `src/app/explore/[id]/page.tsx` | `src/app/explore/[id]/page.test.tsx` | +| `src/store/wallet.ts` | `src/store/wallet.test.ts` | +| `src/app/solve/accept-intent.integration.test.tsx` | co-located under the page dir | ## Naming diff --git a/docs/toast-system.md b/docs/toast-system.md index decf12c..78b0756 100644 --- a/docs/toast-system.md +++ b/docs/toast-system.md @@ -41,7 +41,7 @@ if (latestError) { The `variant` argument is a `ToastVariant`, defaulting to `"info"` when omitted: -| variant | when to use | +| variant | when to use | | --------- | ----------------------------------------- | | `success` | an action completed as expected | | `error` | an action failed and the user should know | diff --git a/docs/websocket-protocol.md b/docs/websocket-protocol.md index 9564359..ca0ad1c 100644 --- a/docs/websocket-protocol.md +++ b/docs/websocket-protocol.md @@ -7,6 +7,7 @@ The frontend connects to a WebSocket endpoint defined by `NEXT_PUBLIC_WS_URL`. T JSON parsing. Each frame is expected to be a JSON object matched to the generic type `T`. **Connection behavior:** + - Reconnect delay: 3 seconds - Malformed frames are silently ignored - Passing `null` as the URL tears down the socket and stays idle @@ -43,16 +44,16 @@ Seeds from REST `/intents/feed` (`src/hooks/useActivityFeed.ts`) and layers live } ``` -| Field | Type | Notes | -|-------|------|-------| -| `id` | `string` | Unique intent identifier | -| `srcChain` | `string` | Source chain identifier | -| `srcToken` | `string` | Source asset symbol | -| `srcAmount` | `string` | Human-readable amount | -| `dstToken` | `string` | Destination asset symbol | -| `solver` | `string` | Solver name or address | -| `status` | `string` | Enum: `pending`, `accepted`, `filled`, `failed` | -| `createdAt` | `string` | ISO-8601 UTC timestamp | +| Field | Type | Notes | +| ----------- | -------- | ----------------------------------------------- | +| `id` | `string` | Unique intent identifier | +| `srcChain` | `string` | Source chain identifier | +| `srcToken` | `string` | Source asset symbol | +| `srcAmount` | `string` | Human-readable amount | +| `dstToken` | `string` | Destination asset symbol | +| `solver` | `string` | Solver name or address | +| `status` | `string` | Enum: `pending`, `accepted`, `filled`, `failed` | +| `createdAt` | `string` | ISO-8601 UTC timestamp | ## App-Wide Status-Change Alerts diff --git a/package-lock.json b/package-lock.json index b59c4b3..0ce94f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^2.1.9", "autoprefixer": "^10", + "editorconfig-checker": "^6.0.0", "eslint": "^8", "eslint-config-next": "14.2.3", "husky": "^9.1.7", @@ -7296,6 +7297,24 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/editorconfig-checker": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/editorconfig-checker/-/editorconfig-checker-6.2.0.tgz", + "integrity": "sha512-5zrNwlxUWyOvAcrSK4mPIGNYrf1KK5pH2D3VXDB1AcDsDdBfLNPOjBL5jHgYh+JykOIuXlQHMeYvXPrZwGNmHA==", + "dev": true, + "license": "MIT", + "bin": { + "ec": "dist/index.js", + "editorconfig-checker": "dist/index.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "funding": { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/mstruebing" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.396", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", diff --git a/package.json b/package.json index 5525362..e170719 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "build": "node scripts/check-env-vars.mjs && next build", "start": "next start", "lint": "next lint", + "check:editorconfig": "editorconfig-checker", + "check:env": "node scripts/check-env-vars.mjs", "typecheck": "tsc --noEmit", "check:env": "node scripts/check-env-vars.mjs", "test": "vitest run", @@ -44,6 +46,7 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^2.1.9", "autoprefixer": "^10", + "editorconfig-checker": "^6.0.0", "eslint": "^8", "eslint-config-next": "14.2.3", "husky": "^9.1.7", diff --git a/scripts/seed-staging-data.mjs b/scripts/seed-staging-data.mjs new file mode 100644 index 0000000..dbb1b0e --- /dev/null +++ b/scripts/seed-staging-data.mjs @@ -0,0 +1,167 @@ +#!/usr/bin/env node +/** + * seed-staging-data.mjs + * + * Generates and seeds realistic synthetic test datasets for the Vortex Staging + * environment. Covers multiple intent lifecycle states (pending, accepted, + * filled, failed) across supported chains (Stellar, Ethereum, Arbitrum, Polygon) + * and solver reputation profiles. + * + * Usage: + * node scripts/seed-staging-data.mjs [--dry-run] [--api-url ] + */ + +const DRY_RUN = process.argv.includes("--dry-run"); +const API_URL_ARG_IDX = process.argv.indexOf("--api-url"); +const TARGET_API_URL = + API_URL_ARG_IDX !== -1 && process.argv[API_URL_ARG_IDX + 1] + ? process.argv[API_URL_ARG_IDX + 1] + : process.env.NEXT_PUBLIC_API_URL || "https://staging-api.vortex-protocol.org"; + +export const SYNTHETIC_SOLVERS = [ + { + name: "Nexus Flow Solver", + address: "GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37", + bondUsd: 250000, + fills: 1420, + failed: 12, + volumeUsd: 3840000, + avgFillTimeSeconds: 4.2, + successRatePct: 99.16, + chains: ["stellar", "ethereum", "arbitrum", "polygon"], + status: "active", + }, + { + name: "Stellar Horizon Arb", + address: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + bondUsd: 100000, + fills: 890, + failed: 35, + volumeUsd: 1450000, + avgFillTimeSeconds: 6.8, + successRatePct: 96.21, + chains: ["stellar", "polygon"], + status: "active", + }, + { + name: "QuickFill Router", + address: "GCZOD2Z3Z7UK6X26YV4VQLDYZF3Y3OBLM2K56P22XU57L4AFLZ34ZAB1", + bondUsd: 50000, + fills: 310, + failed: 48, + volumeUsd: 420000, + avgFillTimeSeconds: 12.5, + successRatePct: 86.59, + chains: ["stellar", "arbitrum"], + status: "active", + }, + { + name: "Soroban Flash Solver", + address: "GAXW7K56K2J37R563K5G7H5N2K4J5L6M7N8O9P0Q1R2S3T4U5V6W7X8Y", + bondUsd: 20000, + fills: 45, + failed: 18, + volumeUsd: 65000, + avgFillTimeSeconds: 18.2, + successRatePct: 71.43, + chains: ["stellar"], + status: "inactive", + }, +]; + +export const SYNTHETIC_INTENTS = [ + { + id: "int_01HZX8P1N5K4L3M2J1", + srcChain: "stellar", + srcToken: "USDC", + srcAmount: "500.00", + dstToken: "XLM", + dstAmount: "4545.45", + minOut: "4500.00", + dstAddress: "GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37", + solver: "GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37", + status: "filled", + createdAt: new Date(Date.now() - 15 * 60 * 1000).toISOString(), + deadline: new Date(Date.now() + 45 * 60 * 1000).toISOString(), + txHash: "7b4e9f1a2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f", + }, + { + id: "int_01HZX8Q2M4K3L2J1N0", + srcChain: "ethereum", + srcToken: "ETH", + srcAmount: "1.25", + dstToken: "USDC", + dstAmount: "4250.00", + minOut: "4200.00", + dstAddress: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + solver: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + status: "accepted", + createdAt: new Date(Date.now() - 3 * 60 * 1000).toISOString(), + deadline: new Date(Date.now() + 27 * 60 * 1000).toISOString(), + }, + { + id: "int_01HZX8R3L3K2J1N0M9", + srcChain: "arbitrum", + srcToken: "ARB", + srcAmount: "2500.00", + dstToken: "USDC", + dstAmount: "1875.00", + minOut: "1850.00", + dstAddress: "GCZOD2Z3Z7UK6X26YV4VQLDYZF3Y3OBLM2K56P22XU57L4AFLZ34ZAB1", + solver: "", + status: "pending", + createdAt: new Date(Date.now() - 1 * 60 * 1000).toISOString(), + deadline: new Date(Date.now() + 59 * 60 * 1000).toISOString(), + }, + { + id: "int_01HZX8S4K2J1N0M9L8", + srcChain: "polygon", + srcToken: "MATIC", + srcAmount: "1500.00", + dstToken: "XLM", + dstAmount: "6800.00", + minOut: "6750.00", + dstAddress: "GAXW7K56K2J37R563K5G7H5N2K4J5L6M7N8O9P0Q1R2S3T4U5V6W7X8Y", + solver: "GAXW7K56K2J37R563K5G7H5N2K4J5L6M7N8O9P0Q1R2S3T4U5V6W7X8Y", + status: "failed", + createdAt: new Date(Date.now() - 120 * 60 * 1000).toISOString(), + deadline: new Date(Date.now() - 60 * 60 * 1000).toISOString(), + }, +]; + +async function main() { + console.log("=================================================="); + console.log(" Vortex Staging Synthetic Data Generator / Seeder"); + console.log("=================================================="); + console.log(`Target Relay API: ${TARGET_API_URL}`); + console.log(`Dry Run Mode: ${DRY_RUN ? "YES" : "NO"}`); + console.log(`Solvers Seeded: ${SYNTHETIC_SOLVERS.length}`); + console.log(`Intents Seeded: ${SYNTHETIC_INTENTS.length}`); + console.log("--------------------------------------------------"); + + if (DRY_RUN) { + console.log("Dry run complete. Payload valid."); + return; + } + + // Attempt staging API upload if endpoint is reachable + try { + const res = await fetch(`${TARGET_API_URL}/admin/seed`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + solvers: SYNTHETIC_SOLVERS, + intents: SYNTHETIC_INTENTS, + }), + }); + if (res.ok) { + console.log("✅ Successfully seeded staging backend instance."); + } else { + console.log(`ℹ️ Staging API /admin/seed returned ${res.status}. Synthetic payloads generated for local staging use.`); + } + } catch (err) { + console.log(`ℹ️ Note: Staging backend not directly reachable (${err.message}). Synthetic seed data structure generated for maintainer usage.`); + } +} + +main(); diff --git a/src/app/explore/ExplorePageClient.tsx b/src/app/explore/ExplorePageClient.tsx index acfa22e..629227d 100644 --- a/src/app/explore/ExplorePageClient.tsx +++ b/src/app/explore/ExplorePageClient.tsx @@ -16,7 +16,13 @@ import { CHAINS } from "@/lib/marketData"; import { sanitizeDisplayText } from "@/lib/textSafety"; import type { IntentStatus } from "@/lib/types"; -const STATUS_OPTIONS: Array = ["all", "pending", "accepted", "filled", "failed"]; +const STATUS_OPTIONS: Array = [ + "all", + "pending", + "accepted", + "filled", + "failed", +]; const SORT_OPTIONS = ["newest", "oldest", "largest"] as const; type SortOption = (typeof SORT_OPTIONS)[number]; const CHAIN_IDS = new Set(CHAINS.map((c) => c.id)); @@ -67,21 +73,39 @@ export default function ExplorePageClient() { const filtered = useMemo(() => { let result = intents; + if (statusFilter !== "all") { result = result.filter((i) => i.status === statusFilter); } if (chainFilter !== "all") { result = result.filter((i) => i.srcChain === chainFilter); } + if (debouncedSearch.trim()) { + const q = debouncedSearch.toLowerCase().trim(); + result = result.filter( + (i) => + i.id.toLowerCase().includes(q) || + i.srcToken.toLowerCase().includes(q) || + i.dstToken.toLowerCase().includes(q) || + i.srcChain.toLowerCase().includes(q) || + i.solver.toLowerCase().includes(q), + ); + } result = [...result].sort((a, b) => { - if (sort === "newest") return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); - if (sort === "oldest") return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + if (sort === "newest") + return ( + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + if (sort === "oldest") + return ( + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); return parseFloat(b.srcAmount) - parseFloat(a.srcAmount); }); return result; - }, [intents, statusFilter, chainFilter, sort]); + }, [intents, debouncedSearch, statusFilter, chainFilter, sort]); // Pagination is superseded by virtualization (#228): the full filtered/sorted // list is windowed instead of paginated, so `page` is intentionally not a URL param. @@ -97,6 +121,14 @@ export default function ExplorePageClient() { rowVirtualizer.scrollToIndex(0); }, [statusFilter, chainFilter, sort, rowVirtualizer]); + const handleExportCsv = () => { + downloadCsv("vortex-intents.csv", buildIntentsCsv(filtered)); + }; + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }; + return (