diff --git a/messages/en.json b/messages/en.json index ec86cb5..66a6585 100644 --- a/messages/en.json +++ b/messages/en.json @@ -398,7 +398,9 @@ "colTags": "Tags", "colActions": "Actions", "actionDrain": "Drain", - "actionRelease": "Release" + "actionRelease": "Release", + "actionRestart": "Restart", + "actionCloseAe": "Close AE" }, "componentsAdminRenderQueueTable": { "emptyState": "No render jobs found for the selected filter.", diff --git a/messages/fa.json b/messages/fa.json index c157575..c221e4a 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -398,7 +398,9 @@ "colTags": "برچسب‌ها", "colActions": "عملیات", "actionDrain": "تخلیه", - "actionRelease": "آزادسازی" + "actionRelease": "آزادسازی", + "actionRestart": "ری‌استارت", + "actionCloseAe": "بستن AE" }, "componentsAdminRenderQueueTable": { "emptyState": "هیچ کار رندری برای فیلتر انتخاب‌شده یافت نشد.", diff --git a/services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs b/services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs index 7d579cb..f9ff05f 100644 --- a/services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs +++ b/services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs @@ -57,6 +57,23 @@ public class AdminService(IdentityDbContext db) pays.Sum(p => p.AmountMinor), payingAllTime, daily); } + // ── Plan statistics ────────────────────────────────────────────────────── + + public async Task> GetPlanStatisticsAsync(Guid tenantId) + { + var now = DateTime.UtcNow; + return await db.UserPlans + .Where(p => p.TenantId == tenantId) + .GroupBy(p => p.PlanName) + .Select(g => new PlanStatRow( + g.Key, + g.Count(), + g.Count(x => x.ExpiresAt > now && x.CancelledAt == null), + g.Sum(x => x.PriceMinorPaid))) + .OrderByDescending(r => r.RevenueMinor) + .ToListAsync(); + } + // ── CRM notes / tags ──────────────────────────────────────────────────── public async Task GetUserCrmAsync(Guid userId) diff --git a/services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs b/services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs index edb9549..1c87cbc 100644 --- a/services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs +++ b/services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs @@ -22,6 +22,10 @@ public class AdminController(AdminService svc) : ControllerBase return Ok(await svc.GetCrmAnalyticsAsync(TenantId, s, e)); } + // ── Plan statistics ────────────────────────────────────────────────────── + [HttpGet("v1/admin/plan-statistics")] + public async Task PlanStats() => Ok(await svc.GetPlanStatisticsAsync(TenantId)); + // ── CRM notes / tags ─────────────────────────────────────────────────────── [HttpGet("v1/users/{userId:guid}/crm")] public async Task GetCrm(Guid userId) => Ok(await svc.GetUserCrmAsync(userId)); diff --git a/services/identity/FlatRender.IdentitySvc/Models/Admin.cs b/services/identity/FlatRender.IdentitySvc/Models/Admin.cs index ebe4c60..57c0cc5 100644 --- a/services/identity/FlatRender.IdentitySvc/Models/Admin.cs +++ b/services/identity/FlatRender.IdentitySvc/Models/Admin.cs @@ -14,6 +14,10 @@ public record CrmAnalyticsResponse( List Daily ); +// ── Plan statistics breakdown ──────────────────────────────────────────────── + +public record PlanStatRow(string PlanName, int Total, int Active, long RevenueMinor); + // ── CRM notes / tags per customer ──────────────────────────────────────────── public record UserCrmResponse(string[] Tags, string? Note, string Status); diff --git a/src/app/api/admin/nodes/[nodeId]/close-ae/route.ts b/src/app/api/admin/nodes/[nodeId]/close-ae/route.ts new file mode 100644 index 0000000..2e9a442 --- /dev/null +++ b/src/app/api/admin/nodes/[nodeId]/close-ae/route.ts @@ -0,0 +1,9 @@ +import { type NextRequest } from "next/server"; +import { adminProxy } from "@/app/api/admin/_adminProxy"; + +export const runtime = "nodejs"; +interface Ctx { params: { nodeId: string } } + +export async function POST(req: NextRequest, { params }: Ctx) { + return adminProxy(req, `/v1/nodes/${params.nodeId}/close-ae`); +} diff --git a/src/app/api/admin/nodes/[nodeId]/restart/route.ts b/src/app/api/admin/nodes/[nodeId]/restart/route.ts new file mode 100644 index 0000000..0b9b13d --- /dev/null +++ b/src/app/api/admin/nodes/[nodeId]/restart/route.ts @@ -0,0 +1,9 @@ +import { type NextRequest } from "next/server"; +import { adminProxy } from "@/app/api/admin/_adminProxy"; + +export const runtime = "nodejs"; +interface Ctx { params: { nodeId: string } } + +export async function POST(req: NextRequest, { params }: Ctx) { + return adminProxy(req, `/v1/nodes/${params.nodeId}/restart`); +} diff --git a/src/components/admin/NodesTable.tsx b/src/components/admin/NodesTable.tsx index a7a7dab..269413f 100644 --- a/src/components/admin/NodesTable.tsx +++ b/src/components/admin/NodesTable.tsx @@ -107,6 +107,20 @@ export function NodesTable({ nodes }: { nodes: V2Node[] }) { > {t("actionDrain")} + +