diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 420c88c..fc11ddf 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -349,12 +349,4 @@ jobs: if: always() run: docker compose -f docker-compose.yml -f docker-compose.admin.yml ps - - name: Prune old meezi images - if: success() - # Only remove untagged (dangling) meezi images — never touches other projects - run: | - docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' \ - | grep '^hostexecutor-' \ - | grep '' \ - | awk '{print $2}' \ - | xargs -r docker rmi || true + # Intentionally no image pruning — disk cleanup is done manually on the server. diff --git a/web/admin/src/components/admin/admin-screens.tsx b/web/admin/src/components/admin/admin-screens.tsx index 8aca463..c25c2ad 100644 --- a/web/admin/src/components/admin/admin-screens.tsx +++ b/web/admin/src/components/admin/admin-screens.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Link } from "@/i18n/routing"; +import { cn } from "@/lib/utils"; import { adminDelete, adminGet, @@ -37,6 +38,30 @@ import { type TicketStatus, } from "@/components/support/ticket-status-badge"; +// iOS-style toggle switch used throughout this file +function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: (v: boolean) => void; disabled?: boolean }) { + return ( + + ); +} + export function AdminDashboardScreen() { const t = useTranslations("admin.dashboard"); const { data } = useQuery({ @@ -78,6 +103,50 @@ function StatCard({ label, value }: { label: string; value: number }) { ); } +function PlanCard({ plan, onSave }: { plan: AdminPlan; onSave: (p: AdminPlan) => void }) { + const t = useTranslations("admin.plans"); + const [price, setPrice] = useState(plan.monthlyPriceToman); + const [maxOrders, setMaxOrders] = useState(plan.limits.maxOrdersPerDay); + + // Sync server values if they change (e.g. after successful save + refetch) + useEffect(() => { setPrice(plan.monthlyPriceToman); }, [plan.monthlyPriceToman]); + useEffect(() => { setMaxOrders(plan.limits.maxOrdersPerDay); }, [plan.limits.maxOrdersPerDay]); + + const flush = () => + onSave({ ...plan, monthlyPriceToman: price, limits: { ...plan.limits, maxOrdersPerDay: maxOrders } }); + + return ( + + + {plan.displayNameFa} +

{plan.tier}

+
+ + + + +
+ ); +} + export function AdminPlansScreen() { const t = useTranslations("admin.plans"); const qc = useQueryClient(); @@ -108,38 +177,7 @@ export function AdminPlansScreen() {

{t("title")}

{plans.map((plan) => ( - - - {plan.displayNameFa} -

{plan.tier}

-
- - - - -
+ save.mutate(p)} /> ))}
); @@ -166,17 +204,34 @@ export function AdminSettingsScreen() {

{t("title")}

- {settings.map((s) => ( - -

{s.key}

-

{s.descriptionFa}

- save.mutate({ key: s.key, value: e.target.value })} - /> -
- ))} + {settings.map((s) => { + const isBool = s.value === "true" || s.value === "false"; + return ( + +
+
+

{s.key}

+ {s.descriptionFa ? ( +

{s.descriptionFa}

+ ) : null} +
+ {isBool ? ( + save.mutate({ key: s.key, value: String(v) })} + disabled={save.isPending} + /> + ) : ( + save.mutate({ key: s.key, value: e.target.value })} + /> + )} +
+
+ ); + })}
); @@ -588,23 +643,21 @@ export function AdminIntegrationsScreen() { {t("active")} ) : null} - + {t("enabled")} + - + {t("sandbox")} + {g.id === "zarinpal" ? (