diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9e8b1a4..f01e6e76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,17 @@ jobs: run: npm run test:coverage working-directory: frontend + - name: Upload frontend coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: frontend/coverage/lcov.info + flags: frontend + name: frontend-coverage + fail_ci_if_error: false + verbose: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Build run: npm run build working-directory: frontend diff --git a/backend/.env.example b/backend/.env.example index 5035a28b..5532d334 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -50,6 +50,9 @@ STREAM_CREATE_RATE_LIMIT=10 # (cancel_stream, top_up_stream). Must be funded on the target network. KEEPER_SECRET_KEY= +# Max stream creation requests per wallet per minute (default: 10) +STREAM_CREATE_RATE_LIMIT=10 + # ─── Auth ───────────────────────────────────────────────────────────────────── # Secret used to sign JWTs (generate with: openssl rand -hex 32) JWT_SECRET= diff --git a/backend/Dockerfile b/backend/Dockerfile index d9520066..b9f1cef5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,6 +8,7 @@ RUN npm install COPY tsconfig.json ./ COPY src ./src COPY prisma ./prisma +COPY prisma.config.ts ./ RUN npm run build diff --git a/backend/package.json b/backend/package.json index 42c27d07..14b73581 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,6 +32,7 @@ "express-rate-limit": "^8.5.2", "ioredis": "^5.11.1", "pg": "^8.21.0", + "stellar-sdk": "^13.3.0", "swagger-jsdoc": "^6.3.0", "swagger-ui-express": "^5.0.1", "winston": "^3.11.0", @@ -46,7 +47,7 @@ "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", - "@vitest/coverage-v8": "^2.1.8", + "@vitest/coverage-v8": "^2.1.9", "eventsource": "^2.0.2", "nodemon": "^3.1.11", "prisma": "^7.8.0", diff --git a/backend/src/app.ts b/backend/src/app.ts index a2bfe853..2dbb531e 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -113,6 +113,17 @@ app.use((req: Request, res: Response, next: NextFunction) => { return next(); // Not versioned, continue to deprecated handlers }); +// Deprecated routes fallback +app.use(['/streams', '/events'], (req: Request, res: Response) => { + res.status(410).json({ + deprecated: true, + migration: { + old: req.originalUrl, + new: `/v1${req.originalUrl}` + } + }); +}); + // Health check routes app.use('/health', healthRoutes); diff --git a/backend/src/controllers/sse.controller.ts b/backend/src/controllers/sse.controller.ts index 9b702257..c569e16f 100644 --- a/backend/src/controllers/sse.controller.ts +++ b/backend/src/controllers/sse.controller.ts @@ -88,6 +88,7 @@ export const subscribe = async (req: Request, res: Response) => { res.write(`data: ${JSON.stringify({ type: 'connected', clientId, requestId })}\n\n`); sseService.addClient(clientId, res, subscriptions, sourceIp); + return; } catch (error: unknown) { if (error instanceof z.ZodError) { diff --git a/backend/src/controllers/stream.controller.ts b/backend/src/controllers/stream.controller.ts index b7670e84..d738de94 100644 --- a/backend/src/controllers/stream.controller.ts +++ b/backend/src/controllers/stream.controller.ts @@ -774,4 +774,4 @@ export const resumeStream = async (req: Request, res: Response) => { logger.error('Error resuming stream:', error); return res.status(500).json({ error: 'Internal server error' }); } -}; \ No newline at end of file +}; diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx index 5dac949f..cc2b760b 100644 --- a/frontend/src/components/dashboard/dashboard-view.tsx +++ b/frontend/src/components/dashboard/dashboard-view.tsx @@ -15,6 +15,7 @@ import toast from "react-hot-toast"; * - Error state: "Failed to load streams" with a retry button */ +import { Skeleton } from "@/components/ui/Skeleton"; import { getDashboardAnalytics, @@ -108,6 +109,8 @@ const SIDEBAR_ITEMS: SidebarItem[] = [ /** Shimmer card used as a placeholder while data loads */ function SkeletonCard({ className = "" }: { className?: string }) { return ( +