From 693f9b4495bf7bffa805b6d2f0459aa4fdc70908 Mon Sep 17 00:00:00 2001 From: Anso Date: Sat, 9 May 2026 01:55:56 -0400 Subject: [PATCH] fix(fleet): drop tier gate from stack and container list endpoints (#1012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Community tier opened the Fleet Overview drilldown (node card → stack list → container list) but two read-only metadata endpoints in fleet.ts kept their requirePaid gate. Stack names still rendered because they ride on the un-gated /api/fleet/overview payload, but expanding a stack always re-fetched containers and hit the orphan gate, surfacing as a "Failed to load containers for X" toast on every node card on Community. Drop requirePaid from GET /node/:nodeId/stacks and GET /node/:nodeId/stacks/:stackName/containers. Both handlers stay behind authMiddleware, validate inputs, and proxy to the per-node api_token for remote nodes. Paid mutations (bulk update, scheduled snapshots, label bulk actions, Fleet Actions cards) keep their gates in fleetActions.ts. Add positive Community-tier tests in fleet.test.ts and clear the stale "Requires Skipper or Admiral license" line from the OpenAPI descriptions. --- backend/src/__tests__/fleet.test.ts | 16 ++++++++++++++++ backend/src/routes/fleet.ts | 4 ---- docs/openapi.yaml | 4 ++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/backend/src/__tests__/fleet.test.ts b/backend/src/__tests__/fleet.test.ts index 7cadb06e..5efe8080 100644 --- a/backend/src/__tests__/fleet.test.ts +++ b/backend/src/__tests__/fleet.test.ts @@ -205,6 +205,22 @@ describe('Fleet tier gating', () => { .set('Authorization', authHeader); expect(res.body.code).not.toBe('PAID_REQUIRED'); }); + + it('GET /api/fleet/node/:nodeId/stacks is accessible on community tier', async () => { + mockTier('community'); + const res = await request(app) + .get('/api/fleet/node/1/stacks') + .set('Authorization', authHeader); + expect(res.body.code).not.toBe('PAID_REQUIRED'); + }); + + it('GET /api/fleet/node/:nodeId/stacks/:stackName/containers is accessible on community tier', async () => { + mockTier('community'); + const res = await request(app) + .get('/api/fleet/node/1/stacks/db-compose/containers') + .set('Authorization', authHeader); + expect(res.body.code).not.toBe('PAID_REQUIRED'); + }); }); // ─── Update Endpoint Input Validation ─── diff --git a/backend/src/routes/fleet.ts b/backend/src/routes/fleet.ts index 65a1465c..6a535d63 100644 --- a/backend/src/routes/fleet.ts +++ b/backend/src/routes/fleet.ts @@ -595,8 +595,6 @@ fleetRouter.get('/configuration', authMiddleware, async (req: Request, res: Resp }); fleetRouter.get('/node/:nodeId/stacks', authMiddleware, async (req: Request, res: Response): Promise => { - if (!requirePaid(req, res)) return; - try { const nodeId = parseIntParam(req, res, 'nodeId', 'node ID'); if (nodeId === null) return; @@ -635,8 +633,6 @@ fleetRouter.get('/node/:nodeId/stacks', authMiddleware, async (req: Request, res }); fleetRouter.get('/node/:nodeId/stacks/:stackName/containers', authMiddleware, async (req: Request, res: Response): Promise => { - if (!requirePaid(req, res)) return; - try { const nodeId = parseIntParam(req, res, 'nodeId', 'node ID'); if (nodeId === null) return; diff --git a/docs/openapi.yaml b/docs/openapi.yaml index a49ce759..346a92b8 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1888,7 +1888,7 @@ paths: operationId: getFleetNodeStacks tags: [Fleet] summary: List stacks on a fleet node - description: Returns stack names from a specific fleet node. Requires Skipper or Admiral license. + description: Returns stack names from a specific fleet node. parameters: - name: nodeId in: path @@ -1928,7 +1928,7 @@ paths: operationId: getFleetNodeStackContainers tags: [Fleet] summary: List containers in a fleet node stack - description: Returns containers for a specific stack on a specific fleet node. Requires Skipper or Admiral license. + description: Returns containers for a specific stack on a specific fleet node. parameters: - name: nodeId in: path