@lukoweb/apitogo-plugin-dev-portal-billing 0.1.88 → 0.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,10 +1,13 @@
1
1
  import * as react_jsx_runtime0 from "react/jsx-runtime";
2
- import * as _lukoweb_apitogo_plugins0 from "@lukoweb/apitogo/plugins";
2
+ import * as _lukoweb_apitogo0 from "@lukoweb/apitogo";
3
3
  import * as _tanstack_react_query0 from "@tanstack/react-query";
4
4
 
5
5
  //#region src/pages/AccountPlansPage.d.ts
6
6
  declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
7
7
  //#endregion
8
+ //#region src/pages/OverviewDashboardPage.d.ts
9
+ declare const OverviewDashboardPage: () => react_jsx_runtime0.JSX.Element;
10
+ //#endregion
8
11
  //#region src/types.d.ts
9
12
  type DevPortalPricingCompareRow = {
10
13
  label: string;
@@ -75,9 +78,10 @@ declare const PRICING_PATH = "/pricing";
75
78
  declare const CHECKOUT_PATH = "/checkout";
76
79
  declare const SUBSCRIPTIONS_PATH = "/account/subscriptions";
77
80
  declare const CREDENTIALS_PATH = "/account/credentials";
81
+ declare const USAGE_PATH = "/account/usage";
78
82
  //#endregion
79
83
  //#region src/DevPortalBillingPlugin.d.ts
80
- declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo_plugins0.ApitogoPlugin;
84
+ declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo0.ApitogoPlugin;
81
85
  //#endregion
82
86
  //#region src/hooks/useApiKeys.d.ts
83
87
  /** Thrown when the developer has hit their plan's per-developer key cap. */
@@ -197,4 +201,4 @@ declare function useCreateApiKey(): _tanstack_react_query0.UseMutationResult<Dev
197
201
  }, unknown>;
198
202
  declare function useRevokeApiKey(): _tanstack_react_query0.UseMutationResult<void, Error, string, unknown>;
199
203
  //#endregion
200
- export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
204
+ export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, OverviewDashboardPage, PRICING_PATH, SUBSCRIPTIONS_PATH, USAGE_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { Button, Head, Heading, Spinner } from "@lukoweb/apitogo/components";
2
- import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon, TrashIcon } from "@lukoweb/apitogo/icons";
1
+ import { Button, Head, Heading, Link, Spinner } from "@lukoweb/apitogo/components";
2
+ import { ArrowRightIcon, CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon, TrashIcon } from "@lukoweb/apitogo/icons";
3
3
  import { createContext, useContext, useEffect, useState } from "react";
4
- import { Link, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
4
+ import { Link as Link$1, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
5
5
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
6
6
  import { cn } from "@lukoweb/apitogo/ui/util.js";
7
7
  import manifestCatalog from "virtual:apitogo-local-manifest";
@@ -9,11 +9,12 @@ import zudokuConfig from "virtual:zudoku-config";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
11
11
  import { useAuth } from "@lukoweb/apitogo/hooks";
12
- import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
13
12
  import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
13
+ import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
14
14
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
15
15
  import { Input } from "@lukoweb/apitogo/ui/Input.js";
16
16
  import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
17
+ import { AccountLayout } from "@lukoweb/apitogo-module-user-panel";
17
18
  import { createPlugin } from "@lukoweb/apitogo/plugins";
18
19
  import { Alert as Alert$1, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
19
20
  import { Button as Button$2 } from "@lukoweb/apitogo/ui/Button";
@@ -154,6 +155,20 @@ function listPlanFeatureLines(plan) {
154
155
  function isActiveSubscription(status) {
155
156
  return status.toLowerCase() === "active";
156
157
  }
158
+ /** Returns monthly call quota from plan limitsJson, or null when unknown/unlimited. */
159
+ function parseMonthlyQuotaCalls(limitsJson) {
160
+ if (!limitsJson?.trim()) return null;
161
+ try {
162
+ const quota = JSON.parse(limitsJson).quota;
163
+ if (!quota || typeof quota !== "object") return null;
164
+ const { calls, renewalPeriodDays } = quota;
165
+ if (calls == null || calls < 0) return null;
166
+ if (renewalPeriodDays == null || renewalPeriodDays === 30 || renewalPeriodDays === 31) return calls;
167
+ return null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
157
172
 
158
173
  //#endregion
159
174
  //#region src/components/PricingCard.tsx
@@ -233,7 +248,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
233
248
  variant: featured ? "default" : "outline",
234
249
  className: "w-full",
235
250
  asChild: true,
236
- children: /* @__PURE__ */ jsx(Link, {
251
+ children: /* @__PURE__ */ jsx(Link$1, {
237
252
  to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
238
253
  children: isPlanFree(plan) ? "Get started" : "Subscribe"
239
254
  })
@@ -241,7 +256,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
241
256
  variant: "secondary",
242
257
  className: "w-full",
243
258
  asChild: true,
244
- children: /* @__PURE__ */ jsx(Link, {
259
+ children: /* @__PURE__ */ jsx(Link$1, {
245
260
  to: "/account/subscriptions",
246
261
  children: "Manage subscription"
247
262
  })
@@ -249,7 +264,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
249
264
  variant: featured ? "default" : "outline",
250
265
  className: "w-full",
251
266
  asChild: true,
252
- children: /* @__PURE__ */ jsx(Link, {
267
+ children: /* @__PURE__ */ jsx(Link$1, {
253
268
  to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
254
269
  children: publicCtaLabel(plan)
255
270
  })
@@ -348,6 +363,9 @@ function createDemoMeResponse(plans) {
348
363
  return {
349
364
  userId: "00000000-0000-0000-0000-000000000001",
350
365
  displayName: "Demo User",
366
+ email: "demo@example.com",
367
+ emailVerified: true,
368
+ hasNativePassword: true,
351
369
  subscriptions: resolveDemoPlans(plans).map((plan) => ({
352
370
  planSku: plan.sku,
353
371
  status: "Active",
@@ -634,6 +652,394 @@ async function readProblemDetail(response) {
634
652
  }
635
653
  }
636
654
 
655
+ //#endregion
656
+ //#region src/hooks/useUsage.ts
657
+ function createDemoUsage(range) {
658
+ const now = /* @__PURE__ */ new Date();
659
+ const days = range === "24h" ? 1 : range === "7d" ? 7 : range === "90d" ? 90 : range === "mtd" ? Math.max(1, now.getUTCDate()) : 30;
660
+ const series = [];
661
+ let total = 0;
662
+ const today = /* @__PURE__ */ new Date();
663
+ for (let i = days - 1; i >= 0; i--) {
664
+ const day = new Date(today);
665
+ day.setUTCDate(day.getUTCDate() - i);
666
+ const requestCount = 40 + i * 17 % 80;
667
+ total += requestCount;
668
+ series.push({
669
+ dayUtc: day.toISOString().slice(0, 10),
670
+ requestCount,
671
+ errorCount: Math.floor(requestCount * .02),
672
+ avgLatencyMs: 40 + i % 20
673
+ });
674
+ }
675
+ return {
676
+ range,
677
+ fromDayUtc: series[0]?.dayUtc ?? today.toISOString().slice(0, 10),
678
+ toDayUtc: series.at(-1)?.dayUtc ?? today.toISOString().slice(0, 10),
679
+ requestCount: total,
680
+ avgLatencyMs: 48,
681
+ errorRate: .018,
682
+ series,
683
+ byEndpoint: [
684
+ {
685
+ path: "GET /pets",
686
+ count: Math.floor(total * .45)
687
+ },
688
+ {
689
+ path: "POST /pets",
690
+ count: Math.floor(total * .25)
691
+ },
692
+ {
693
+ path: "GET /pets/{id}",
694
+ count: Math.floor(total * .2)
695
+ },
696
+ {
697
+ path: "DELETE /pets/{id}",
698
+ count: Math.floor(total * .1)
699
+ }
700
+ ],
701
+ byStatus: [
702
+ {
703
+ label: "2xx",
704
+ count: Math.floor(total * .96)
705
+ },
706
+ {
707
+ label: "4xx",
708
+ count: Math.floor(total * .03)
709
+ },
710
+ {
711
+ label: "5xx",
712
+ count: Math.floor(total * .01)
713
+ }
714
+ ]
715
+ };
716
+ }
717
+ function createDemoRecent() {
718
+ const now = Date.now();
719
+ return { items: [
720
+ {
721
+ occurredAtUtc: (/* @__PURE__ */ new Date(now - 6e4)).toISOString(),
722
+ method: "GET",
723
+ path: "/pets",
724
+ statusCode: 200,
725
+ latencyMs: 42
726
+ },
727
+ {
728
+ occurredAtUtc: (/* @__PURE__ */ new Date(now - 12e4)).toISOString(),
729
+ method: "POST",
730
+ path: "/pets",
731
+ statusCode: 201,
732
+ latencyMs: 88
733
+ },
734
+ {
735
+ occurredAtUtc: (/* @__PURE__ */ new Date(now - 18e4)).toISOString(),
736
+ method: "GET",
737
+ path: "/pets/1",
738
+ statusCode: 404,
739
+ latencyMs: 31
740
+ }
741
+ ] };
742
+ }
743
+ function useUsage(range = "30d") {
744
+ const auth = useAuth();
745
+ const apiBaseUrl = useDevPortalApiBaseUrl();
746
+ const isPreview = useDevPortalPreviewMode();
747
+ const isDemoSession = useIsDemoSession();
748
+ const liveQuery = useQuery({
749
+ queryKey: [
750
+ "dev-portal-usage",
751
+ apiBaseUrl,
752
+ range
753
+ ],
754
+ enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
755
+ queryFn: async () => {
756
+ const response = await fetch(`${apiBaseUrl}/api/v1/usage?range=${encodeURIComponent(range)}`, {
757
+ method: "GET",
758
+ headers: { Accept: "application/json" },
759
+ credentials: "include"
760
+ });
761
+ if (!response.ok) throw new Error("Failed to load usage.");
762
+ return await response.json();
763
+ }
764
+ });
765
+ if (isDemoSession && auth.isAuthenticated) return {
766
+ ...liveQuery,
767
+ data: createDemoUsage(range),
768
+ error: null,
769
+ isError: false,
770
+ isFetching: false,
771
+ isLoading: false,
772
+ isPending: false,
773
+ isSuccess: true,
774
+ status: "success"
775
+ };
776
+ return liveQuery;
777
+ }
778
+ function useUsageRecent(limit = 20) {
779
+ const auth = useAuth();
780
+ const apiBaseUrl = useDevPortalApiBaseUrl();
781
+ const isPreview = useDevPortalPreviewMode();
782
+ const isDemoSession = useIsDemoSession();
783
+ const liveQuery = useQuery({
784
+ queryKey: [
785
+ "dev-portal-usage-recent",
786
+ apiBaseUrl,
787
+ limit
788
+ ],
789
+ enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
790
+ queryFn: async () => {
791
+ const response = await fetch(`${apiBaseUrl}/api/v1/usage/recent?limit=${limit}`, {
792
+ method: "GET",
793
+ headers: { Accept: "application/json" },
794
+ credentials: "include"
795
+ });
796
+ if (!response.ok) throw new Error("Failed to load recent requests.");
797
+ return await response.json();
798
+ }
799
+ });
800
+ if (isDemoSession && auth.isAuthenticated) return {
801
+ ...liveQuery,
802
+ data: createDemoRecent(),
803
+ error: null,
804
+ isError: false,
805
+ isFetching: false,
806
+ isLoading: false,
807
+ isPending: false,
808
+ isSuccess: true,
809
+ status: "success"
810
+ };
811
+ return liveQuery;
812
+ }
813
+
814
+ //#endregion
815
+ //#region src/paths.ts
816
+ const PRICING_PATH = "/pricing";
817
+ const CHECKOUT_PATH = "/checkout";
818
+ const SUBSCRIPTIONS_PATH = "/account/subscriptions";
819
+ const CREDENTIALS_PATH = "/account/credentials";
820
+ const USAGE_PATH = "/account/usage";
821
+ const LOGIN_NAV_PATH = "/signin";
822
+
823
+ //#endregion
824
+ //#region src/pages/OverviewDashboardPage.tsx
825
+ function StatCard({ label, value, sub, subClassName }) {
826
+ return /* @__PURE__ */ jsxs("div", {
827
+ className: "rounded-xl border border-border bg-card p-5",
828
+ children: [
829
+ /* @__PURE__ */ jsx("div", {
830
+ className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
831
+ children: label
832
+ }),
833
+ /* @__PURE__ */ jsx("div", {
834
+ className: "mt-1.5 text-[1.7rem] font-bold tracking-[-0.02em] text-foreground",
835
+ children: value
836
+ }),
837
+ /* @__PURE__ */ jsx("div", {
838
+ className: `mt-1 text-xs ${subClassName ?? "text-muted-foreground"}`,
839
+ children: sub
840
+ })
841
+ ]
842
+ });
843
+ }
844
+ function formatNumber$1(value) {
845
+ return new Intl.NumberFormat().format(value);
846
+ }
847
+ const OverviewDashboardPage = () => {
848
+ const auth = useAuth();
849
+ const { data: apiKeys, isLoading: keysLoading } = useApiKeys();
850
+ const { data: me, isLoading: subsLoading } = useMySubscriptions();
851
+ const { data: catalog } = usePlans();
852
+ const { data: usage } = useUsage("30d");
853
+ const { data: recent } = useUsageRecent(8);
854
+ const activeKeyCount = apiKeys?.plans.reduce((total, plan) => total + plan.keys.length, 0) ?? 0;
855
+ const activeSubscription = (me?.subscriptions ?? []).find((sub) => isActiveSubscription(sub.status));
856
+ const planSku = activeSubscription?.planSku;
857
+ const planName = catalog?.plans.find((plan) => plan.sku === planSku)?.displayName ?? planSku ?? "—";
858
+ const renewLabel = activeSubscription?.currentPeriodEnd ? `Renews ${new Date(activeSubscription.currentPeriodEnd).toLocaleDateString(void 0, {
859
+ month: "short",
860
+ day: "numeric"
861
+ })}` : "No active subscription";
862
+ const statsLoading = auth.isAuthenticated && (keysLoading || subsLoading) && !apiKeys && !me;
863
+ const chartSeries = (usage?.series ?? []).slice(-14);
864
+ const maxBar = Math.max(1, ...chartSeries.map((point) => point.requestCount));
865
+ return /* @__PURE__ */ jsxs("div", {
866
+ className: "px-8 py-7",
867
+ children: [
868
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Overview" }) }),
869
+ /* @__PURE__ */ jsx("h1", {
870
+ className: "mb-6 text-[1.6rem] font-bold tracking-[-0.01em] text-foreground",
871
+ children: "Overview"
872
+ }),
873
+ /* @__PURE__ */ jsxs("div", {
874
+ className: "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
875
+ children: [
876
+ /* @__PURE__ */ jsx(StatCard, {
877
+ label: "Requests (30d)",
878
+ value: usage ? formatNumber$1(usage.requestCount) : statsLoading ? "…" : "—",
879
+ sub: usage ? `${formatNumber$1(usage.series.reduce((sum, p) => sum + p.errorCount, 0))} errors` : "No usage data yet"
880
+ }),
881
+ /* @__PURE__ */ jsx(StatCard, {
882
+ label: "Active keys",
883
+ value: statsLoading ? "…" : String(activeKeyCount),
884
+ sub: activeKeyCount === 1 ? "1 key on your account" : `${activeKeyCount} keys on your account`
885
+ }),
886
+ /* @__PURE__ */ jsx(StatCard, {
887
+ label: "Plan",
888
+ value: statsLoading ? "…" : planName,
889
+ sub: renewLabel
890
+ }),
891
+ /* @__PURE__ */ jsx(StatCard, {
892
+ label: "Error rate",
893
+ value: usage ? `${(usage.errorRate * 100).toFixed(usage.errorRate >= .1 ? 1 : 2)}%` : "—",
894
+ sub: usage?.avgLatencyMs != null ? `${usage.avgLatencyMs} ms avg latency` : "No telemetry yet"
895
+ })
896
+ ]
897
+ }),
898
+ /* @__PURE__ */ jsxs("div", {
899
+ className: "mb-5 grid grid-cols-1 gap-5 lg:grid-cols-2",
900
+ children: [/* @__PURE__ */ jsxs("div", {
901
+ className: "rounded-xl border border-border bg-card p-6",
902
+ children: [/* @__PURE__ */ jsxs("div", {
903
+ className: "mb-4 flex items-center justify-between gap-3",
904
+ children: [/* @__PURE__ */ jsx("h2", {
905
+ className: "text-lg font-semibold text-foreground",
906
+ children: "Requests, last 14 days"
907
+ }), /* @__PURE__ */ jsx(Link, {
908
+ to: USAGE_PATH,
909
+ className: "text-sm font-semibold text-primary hover:underline",
910
+ children: "View usage"
911
+ })]
912
+ }), chartSeries.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
913
+ className: "flex h-[150px] items-end gap-1.5",
914
+ children: chartSeries.map((point) => /* @__PURE__ */ jsx("div", {
915
+ title: `${point.dayUtc}: ${point.requestCount}`,
916
+ className: "min-w-0 flex-1 rounded-t-sm bg-primary",
917
+ style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
918
+ }, point.dayUtc))
919
+ }) : /* @__PURE__ */ jsx("div", {
920
+ className: "flex min-h-[150px] items-center justify-center rounded-lg border border-dashed border-border bg-muted/30 px-4 text-center text-sm text-muted-foreground",
921
+ children: "No request traffic in the last 14 days."
922
+ })]
923
+ }), /* @__PURE__ */ jsxs("div", {
924
+ className: "rounded-xl border border-border bg-card p-6",
925
+ children: [/* @__PURE__ */ jsx("h2", {
926
+ className: "mb-4 text-lg font-semibold text-foreground",
927
+ children: "Quick links"
928
+ }), /* @__PURE__ */ jsxs("div", {
929
+ className: "flex flex-col gap-3",
930
+ children: [
931
+ /* @__PURE__ */ jsx(Button$1, {
932
+ asChild: true,
933
+ variant: "outline",
934
+ className: "justify-between",
935
+ children: /* @__PURE__ */ jsxs(Link, {
936
+ to: CREDENTIALS_PATH,
937
+ children: ["Manage API keys", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
938
+ })
939
+ }),
940
+ /* @__PURE__ */ jsx(Button$1, {
941
+ asChild: true,
942
+ variant: "outline",
943
+ className: "justify-between",
944
+ children: /* @__PURE__ */ jsxs(Link, {
945
+ to: USAGE_PATH,
946
+ children: ["Usage analytics", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
947
+ })
948
+ }),
949
+ /* @__PURE__ */ jsx(Button$1, {
950
+ asChild: true,
951
+ variant: "outline",
952
+ className: "justify-between",
953
+ children: /* @__PURE__ */ jsxs(Link, {
954
+ to: SUBSCRIPTIONS_PATH,
955
+ children: ["Billing & subscriptions", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
956
+ })
957
+ }),
958
+ /* @__PURE__ */ jsx(Button$1, {
959
+ asChild: true,
960
+ variant: "outline",
961
+ className: "justify-between",
962
+ children: /* @__PURE__ */ jsxs(Link, {
963
+ to: "/introduction",
964
+ children: ["Documentation", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
965
+ })
966
+ })
967
+ ]
968
+ })]
969
+ })]
970
+ }),
971
+ /* @__PURE__ */ jsxs("div", {
972
+ className: "rounded-xl border border-border bg-card p-6",
973
+ children: [/* @__PURE__ */ jsx("h2", {
974
+ className: "mb-4 text-lg font-semibold text-foreground",
975
+ children: "Recent requests"
976
+ }), recent && recent.items.length > 0 ? /* @__PURE__ */ jsx("div", {
977
+ className: "overflow-x-auto",
978
+ children: /* @__PURE__ */ jsxs("table", {
979
+ className: "w-full text-left text-sm",
980
+ children: [/* @__PURE__ */ jsx("thead", {
981
+ className: "text-xs uppercase tracking-wide text-muted-foreground",
982
+ children: /* @__PURE__ */ jsxs("tr", { children: [
983
+ /* @__PURE__ */ jsx("th", {
984
+ className: "pb-2 font-medium",
985
+ children: "Time"
986
+ }),
987
+ /* @__PURE__ */ jsx("th", {
988
+ className: "pb-2 font-medium",
989
+ children: "Method"
990
+ }),
991
+ /* @__PURE__ */ jsx("th", {
992
+ className: "pb-2 font-medium",
993
+ children: "Path"
994
+ }),
995
+ /* @__PURE__ */ jsx("th", {
996
+ className: "pb-2 font-medium",
997
+ children: "Status"
998
+ }),
999
+ /* @__PURE__ */ jsx("th", {
1000
+ className: "pb-2 font-medium",
1001
+ children: "Latency"
1002
+ })
1003
+ ] })
1004
+ }), /* @__PURE__ */ jsx("tbody", { children: recent.items.map((item) => /* @__PURE__ */ jsxs("tr", {
1005
+ className: "border-t border-border",
1006
+ children: [
1007
+ /* @__PURE__ */ jsx("td", {
1008
+ className: "py-2 text-muted-foreground",
1009
+ children: new Date(item.occurredAtUtc).toLocaleString()
1010
+ }),
1011
+ /* @__PURE__ */ jsx("td", {
1012
+ className: "py-2 font-mono",
1013
+ children: item.method
1014
+ }),
1015
+ /* @__PURE__ */ jsx("td", {
1016
+ className: "py-2 font-mono",
1017
+ children: item.path
1018
+ }),
1019
+ /* @__PURE__ */ jsx("td", {
1020
+ className: "py-2",
1021
+ children: item.statusCode
1022
+ }),
1023
+ /* @__PURE__ */ jsx("td", {
1024
+ className: "py-2 text-muted-foreground",
1025
+ children: item.latencyMs != null ? `${item.latencyMs} ms` : "—"
1026
+ })
1027
+ ]
1028
+ }, `${item.occurredAtUtc}-${item.method}-${item.path}`)) })]
1029
+ })
1030
+ }) : /* @__PURE__ */ jsx("p", {
1031
+ className: "text-sm text-muted-foreground",
1032
+ children: "No recent requests yet."
1033
+ })]
1034
+ }),
1035
+ !auth.isAuthenticated && auth.isAuthEnabled ? /* @__PURE__ */ jsx("p", {
1036
+ className: "mt-6 text-sm text-muted-foreground",
1037
+ children: "Sign in to see personalized account metrics."
1038
+ }) : null
1039
+ ]
1040
+ });
1041
+ };
1042
+
637
1043
  //#endregion
638
1044
  //#region src/components/ApiKeysPanel.tsx
639
1045
  /**
@@ -858,14 +1264,6 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
858
1264
  });
859
1265
  };
860
1266
 
861
- //#endregion
862
- //#region src/paths.ts
863
- const PRICING_PATH = "/pricing";
864
- const CHECKOUT_PATH = "/checkout";
865
- const SUBSCRIPTIONS_PATH = "/account/subscriptions";
866
- const CREDENTIALS_PATH = "/account/credentials";
867
- const LOGIN_NAV_PATH = "/signin";
868
-
869
1267
  //#endregion
870
1268
  //#region src/pages/CheckoutPage.tsx
871
1269
  const ACCOUNT_PLANS_PATH = "/account/plans";
@@ -913,7 +1311,7 @@ const CheckoutPage = () => {
913
1311
  children: [
914
1312
  "Missing plan SKU.",
915
1313
  " ",
916
- /* @__PURE__ */ jsx(Link, {
1314
+ /* @__PURE__ */ jsx(Link$1, {
917
1315
  to: plansPath,
918
1316
  className: "text-primary underline",
919
1317
  children: "Back to plans"
@@ -934,7 +1332,7 @@ const CheckoutPage = () => {
934
1332
  /* @__PURE__ */ jsx(Button$2, {
935
1333
  asChild: true,
936
1334
  variant: "outline",
937
- children: /* @__PURE__ */ jsx(Link, {
1335
+ children: /* @__PURE__ */ jsx(Link$1, {
938
1336
  to: "/account/subscriptions",
939
1337
  children: "View demo subscriptions"
940
1338
  })
@@ -955,7 +1353,7 @@ const CheckoutPage = () => {
955
1353
  /* @__PURE__ */ jsx(Button$2, {
956
1354
  asChild: true,
957
1355
  variant: "outline",
958
- children: /* @__PURE__ */ jsx(Link, {
1356
+ children: /* @__PURE__ */ jsx(Link$1, {
959
1357
  to: plansPath,
960
1358
  children: "Back to plans"
961
1359
  })
@@ -977,7 +1375,7 @@ const CheckoutPage = () => {
977
1375
  variant: "outline",
978
1376
  size: "xs",
979
1377
  asChild: true,
980
- children: /* @__PURE__ */ jsx(Link, {
1378
+ children: /* @__PURE__ */ jsx(Link$1, {
981
1379
  to: plansPath,
982
1380
  children: "Back to plans"
983
1381
  })
@@ -1073,7 +1471,7 @@ const CredentialsPage = () => {
1073
1471
  }), /* @__PURE__ */ jsx(Button$2, {
1074
1472
  className: "mt-4",
1075
1473
  asChild: true,
1076
- children: /* @__PURE__ */ jsx(Link, {
1474
+ children: /* @__PURE__ */ jsx(Link$1, {
1077
1475
  to: "/account/plans",
1078
1476
  children: "View plans"
1079
1477
  })
@@ -1091,7 +1489,7 @@ const CredentialsPage = () => {
1091
1489
  }), /* @__PURE__ */ jsx(Button$2, {
1092
1490
  className: "mt-4",
1093
1491
  asChild: true,
1094
- children: /* @__PURE__ */ jsx(Link, {
1492
+ children: /* @__PURE__ */ jsx(Link$1, {
1095
1493
  to: "/account/plans",
1096
1494
  children: "View plans"
1097
1495
  })
@@ -1107,7 +1505,7 @@ const CredentialsPage = () => {
1107
1505
  }), /* @__PURE__ */ jsx(Button$2, {
1108
1506
  className: "mt-4",
1109
1507
  asChild: true,
1110
- children: /* @__PURE__ */ jsx(Link, {
1508
+ children: /* @__PURE__ */ jsx(Link$1, {
1111
1509
  to: "/account/plans",
1112
1510
  children: "View plans"
1113
1511
  })
@@ -1141,14 +1539,14 @@ const CredentialsPage = () => {
1141
1539
  children: [/* @__PURE__ */ jsx(Button$2, {
1142
1540
  variant: "outline",
1143
1541
  asChild: true,
1144
- children: /* @__PURE__ */ jsx(Link, {
1542
+ children: /* @__PURE__ */ jsx(Link$1, {
1145
1543
  to: "/account/subscriptions",
1146
1544
  children: "My subscriptions"
1147
1545
  })
1148
1546
  }), /* @__PURE__ */ jsx(Button$2, {
1149
1547
  variant: "outline",
1150
1548
  asChild: true,
1151
- children: /* @__PURE__ */ jsx(Link, {
1549
+ children: /* @__PURE__ */ jsx(Link$1, {
1152
1550
  to: "/api",
1153
1551
  children: "API reference"
1154
1552
  })
@@ -1381,6 +1779,28 @@ const PricingPage = () => {
1381
1779
  });
1382
1780
  };
1383
1781
 
1782
+ //#endregion
1783
+ //#region src/hooks/useBillingPortal.ts
1784
+ function useBillingPortal() {
1785
+ const apiBaseUrl = useDevPortalApiBaseUrl();
1786
+ return useMutation({ mutationFn: async (returnUrl) => {
1787
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
1788
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
1789
+ method: "POST",
1790
+ headers: {
1791
+ Accept: "application/json",
1792
+ "Content-Type": "application/json"
1793
+ },
1794
+ credentials: "include",
1795
+ body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
1796
+ });
1797
+ if (!response.ok) throw new Error("Unable to open billing portal.");
1798
+ const body = await response.json();
1799
+ if (!body.url) throw new Error("Billing portal URL was not returned.");
1800
+ window.location.assign(body.url);
1801
+ } });
1802
+ }
1803
+
1384
1804
  //#endregion
1385
1805
  //#region src/pages/SubscriptionsPage.tsx
1386
1806
  const SubscriptionsPage = () => {
@@ -1389,28 +1809,33 @@ const SubscriptionsPage = () => {
1389
1809
  const checkoutSuccess = searchParams.get("checkout") === "success";
1390
1810
  const { data: me, isLoading } = useMySubscriptions();
1391
1811
  const { data: catalog } = usePlans();
1812
+ const billingPortal = useBillingPortal();
1813
+ const { data: usage } = useUsage("mtd");
1392
1814
  const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
1393
1815
  const subscriptions = me?.subscriptions ?? [];
1816
+ const activeSubscription = subscriptions.find((sub) => isActiveSubscription(sub.status));
1817
+ const activePlan = activeSubscription ? planBySku.get(activeSubscription.planSku) : void 0;
1818
+ const monthlyQuota = activePlan ? parseMonthlyQuotaCalls(activePlan.limitsJson) : null;
1819
+ const usedRequests = usage?.requestCount ?? 0;
1820
+ const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
1394
1821
  return /* @__PURE__ */ jsxs("div", {
1395
- className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
1822
+ className: "px-8 py-7",
1396
1823
  children: [
1397
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "My subscriptions" }) }),
1398
- /* @__PURE__ */ jsxs("div", {
1399
- className: "mb-8 space-y-2",
1400
- children: [/* @__PURE__ */ jsx("h1", {
1401
- className: "text-3xl font-semibold tracking-tight",
1402
- children: "My subscriptions"
1403
- }), /* @__PURE__ */ jsx("p", {
1404
- className: "text-muted-foreground",
1405
- children: "Review your active plans and manage billing."
1406
- })]
1824
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
1825
+ /* @__PURE__ */ jsx("h1", {
1826
+ className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
1827
+ children: "Billing"
1828
+ }),
1829
+ /* @__PURE__ */ jsx("p", {
1830
+ className: "mb-8 text-muted-foreground",
1831
+ children: "Manage your plan, payment method, and invoices."
1407
1832
  }),
1408
1833
  checkoutSuccess ? /* @__PURE__ */ jsxs("div", {
1409
1834
  className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
1410
1835
  children: [
1411
1836
  "Your subscription is active.",
1412
1837
  " ",
1413
- /* @__PURE__ */ jsx(Link, {
1838
+ /* @__PURE__ */ jsx(Link$1, {
1414
1839
  to: CREDENTIALS_PATH,
1415
1840
  className: "font-medium underline",
1416
1841
  children: "View your API credentials"
@@ -1420,32 +1845,80 @@ const SubscriptionsPage = () => {
1420
1845
  }) : null,
1421
1846
  isLoading ? /* @__PURE__ */ jsx("p", {
1422
1847
  className: "text-muted-foreground",
1423
- children: "Loading subscriptions…"
1848
+ children: "Loading billing details…"
1849
+ }) : null,
1850
+ !isLoading && activePlan && activeSubscription ? /* @__PURE__ */ jsxs("div", {
1851
+ className: "mb-8 rounded-xl border border-border bg-card p-6",
1852
+ children: [
1853
+ /* @__PURE__ */ jsx("p", {
1854
+ className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
1855
+ children: "Current plan"
1856
+ }),
1857
+ /* @__PURE__ */ jsx("h2", {
1858
+ className: "mt-2 text-2xl font-bold",
1859
+ children: activePlan.displayName
1860
+ }),
1861
+ /* @__PURE__ */ jsxs("p", {
1862
+ className: "mt-1 text-sm text-muted-foreground",
1863
+ children: [
1864
+ formatPlanPrice(activePlan),
1865
+ " ·",
1866
+ " ",
1867
+ formatBillingPeriod(activePlan.billingPeriod)
1868
+ ]
1869
+ }),
1870
+ activeSubscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
1871
+ className: "mt-3 text-sm text-muted-foreground",
1872
+ children: [
1873
+ "Renews",
1874
+ " ",
1875
+ new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()
1876
+ ]
1877
+ }) : null,
1878
+ quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
1879
+ className: "mt-5",
1880
+ children: [/* @__PURE__ */ jsxs("div", {
1881
+ className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
1882
+ children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
1883
+ usedRequests.toLocaleString(),
1884
+ " /",
1885
+ " ",
1886
+ monthlyQuota.toLocaleString()
1887
+ ] })]
1888
+ }), /* @__PURE__ */ jsx("div", {
1889
+ className: "h-2 rounded-full bg-muted",
1890
+ children: /* @__PURE__ */ jsx("div", {
1891
+ className: "h-full rounded-full bg-primary transition-[width]",
1892
+ style: { width: `${quotaPct}%` }
1893
+ })
1894
+ })]
1895
+ }) : null
1896
+ ]
1424
1897
  }) : null,
1425
1898
  !isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
1426
- className: "rounded-xl border border-dashed p-8 text-center",
1899
+ className: "mb-8 rounded-xl border border-dashed p-8 text-center",
1427
1900
  children: [/* @__PURE__ */ jsx("p", {
1428
1901
  className: "text-muted-foreground",
1429
1902
  children: "You do not have an active subscription yet."
1430
- }), /* @__PURE__ */ jsx(Button$2, {
1903
+ }), /* @__PURE__ */ jsx(Button$1, {
1431
1904
  className: "mt-4",
1432
1905
  asChild: true,
1433
- children: /* @__PURE__ */ jsx(Link, {
1906
+ children: /* @__PURE__ */ jsx(Link$1, {
1434
1907
  to: "/account/plans",
1435
1908
  children: "View plans"
1436
1909
  })
1437
1910
  })]
1438
1911
  }) : null,
1439
- subscriptions.length > 0 ? /* @__PURE__ */ jsx("div", {
1440
- className: "space-y-4",
1912
+ !isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
1913
+ className: "mb-8 space-y-4",
1441
1914
  children: subscriptions.map((subscription) => {
1442
1915
  const plan = planBySku.get(subscription.planSku);
1443
- return /* @__PURE__ */ jsxs("article", {
1444
- className: "rounded-xl border bg-card/60 p-6",
1445
- children: [/* @__PURE__ */ jsxs("div", {
1916
+ return /* @__PURE__ */ jsx("article", {
1917
+ className: "rounded-xl border bg-card p-5",
1918
+ children: /* @__PURE__ */ jsxs("div", {
1446
1919
  className: "flex flex-wrap items-start justify-between gap-4",
1447
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
1448
- className: "text-lg font-medium",
1920
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
1921
+ className: "font-semibold",
1449
1922
  children: plan?.displayName ?? subscription.planSku
1450
1923
  }), plan ? /* @__PURE__ */ jsxs("p", {
1451
1924
  className: "mt-1 text-sm text-muted-foreground",
@@ -1456,48 +1929,238 @@ const SubscriptionsPage = () => {
1456
1929
  formatBillingPeriod(plan.billingPeriod)
1457
1930
  ]
1458
1931
  }) : null] }), /* @__PURE__ */ jsx("span", {
1459
- className: isActiveSubscription(subscription.status) ? "rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary" : "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
1932
+ className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
1460
1933
  children: subscription.status
1461
1934
  })]
1462
- }), subscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
1463
- className: "mt-3 text-sm text-muted-foreground",
1464
- children: [
1465
- "Current period ends",
1466
- " ",
1467
- new Date(subscription.currentPeriodEnd).toLocaleDateString()
1468
- ]
1469
- }) : null]
1935
+ })
1470
1936
  }, `${subscription.planSku}-${subscription.status}`);
1471
1937
  })
1472
1938
  }) : null,
1473
1939
  /* @__PURE__ */ jsxs("div", {
1474
- className: "mt-8 flex flex-wrap gap-3",
1940
+ className: "flex flex-wrap gap-3",
1475
1941
  children: [
1476
- /* @__PURE__ */ jsx(Button$2, {
1942
+ /* @__PURE__ */ jsx(Button$1, {
1477
1943
  variant: "outline",
1478
1944
  asChild: true,
1479
- children: /* @__PURE__ */ jsx(Link, {
1945
+ children: /* @__PURE__ */ jsx(Link$1, {
1480
1946
  to: "/account/plans",
1481
1947
  children: "Change plan"
1482
1948
  })
1483
1949
  }),
1484
- /* @__PURE__ */ jsx(Button$2, {
1485
- variant: "outline",
1486
- asChild: true,
1487
- children: /* @__PURE__ */ jsx(Link, {
1488
- to: "/account/plans",
1489
- children: "Billing settings"
1490
- })
1491
- }),
1492
- auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$2, {
1950
+ auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
1951
+ variant: "default",
1952
+ disabled: billingPortal.isPending,
1953
+ onClick: () => {
1954
+ billingPortal.mutate();
1955
+ },
1956
+ children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
1957
+ }) : null,
1958
+ auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
1493
1959
  variant: "secondary",
1494
1960
  asChild: true,
1495
- children: /* @__PURE__ */ jsx(Link, {
1961
+ children: /* @__PURE__ */ jsx(Link$1, {
1496
1962
  to: CREDENTIALS_PATH,
1497
1963
  children: "API credentials"
1498
1964
  })
1499
1965
  }) : null
1500
1966
  ]
1967
+ }),
1968
+ billingPortal.isError ? /* @__PURE__ */ jsx("p", {
1969
+ className: "mt-4 text-sm text-destructive",
1970
+ children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
1971
+ }) : null
1972
+ ]
1973
+ });
1974
+ };
1975
+
1976
+ //#endregion
1977
+ //#region src/pages/UsagePage.tsx
1978
+ const RANGES = [
1979
+ {
1980
+ value: "24h",
1981
+ label: "24h"
1982
+ },
1983
+ {
1984
+ value: "7d",
1985
+ label: "7d"
1986
+ },
1987
+ {
1988
+ value: "30d",
1989
+ label: "30d"
1990
+ },
1991
+ {
1992
+ value: "90d",
1993
+ label: "90d"
1994
+ }
1995
+ ];
1996
+ function formatNumber(value) {
1997
+ return new Intl.NumberFormat().format(value);
1998
+ }
1999
+ function formatPct(rate) {
2000
+ return `${(rate * 100).toFixed(rate >= .1 ? 1 : 2)}%`;
2001
+ }
2002
+ const UsagePage = () => {
2003
+ const auth = useAuth();
2004
+ const [range, setRange] = useState("30d");
2005
+ const { data, isLoading, isError } = useUsage(range);
2006
+ const maxBar = Math.max(1, ...data?.series.map((point) => point.requestCount) ?? [1]);
2007
+ const endpointTotal = data?.byEndpoint.reduce((sum, item) => sum + item.count, 0) ?? 0;
2008
+ if (!auth.isAuthenticated) return /* @__PURE__ */ jsxs("div", {
2009
+ className: "px-8 py-7",
2010
+ children: [
2011
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
2012
+ /* @__PURE__ */ jsx("h1", {
2013
+ className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
2014
+ children: "Usage"
2015
+ }),
2016
+ /* @__PURE__ */ jsx("p", {
2017
+ className: "text-muted-foreground",
2018
+ children: "Sign in to view your API usage."
2019
+ }),
2020
+ auth.isAuthEnabled ? /* @__PURE__ */ jsx(Button$1, {
2021
+ asChild: true,
2022
+ className: "mt-4",
2023
+ children: /* @__PURE__ */ jsx(Link$1, {
2024
+ to: "/signin",
2025
+ children: "Sign in"
2026
+ })
2027
+ }) : null
2028
+ ]
2029
+ });
2030
+ return /* @__PURE__ */ jsxs("div", {
2031
+ className: "px-8 py-7",
2032
+ children: [
2033
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
2034
+ /* @__PURE__ */ jsxs("div", {
2035
+ className: "mb-6 flex flex-wrap items-center justify-between gap-3",
2036
+ children: [/* @__PURE__ */ jsx("h1", {
2037
+ className: "text-[1.6rem] font-bold tracking-[-0.01em]",
2038
+ children: "Usage"
2039
+ }), /* @__PURE__ */ jsx("div", {
2040
+ className: "flex gap-1 rounded-lg border border-border bg-card p-1",
2041
+ children: RANGES.map((item) => /* @__PURE__ */ jsx("button", {
2042
+ type: "button",
2043
+ onClick: () => setRange(item.value),
2044
+ className: `rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${range === item.value ? "bg-primary/14 text-primary" : "text-muted-foreground hover:text-foreground"}`,
2045
+ children: item.label
2046
+ }, item.value))
2047
+ })]
2048
+ }),
2049
+ isLoading ? /* @__PURE__ */ jsx("p", {
2050
+ className: "text-sm text-muted-foreground",
2051
+ children: "Loading usage…"
2052
+ }) : null,
2053
+ isError ? /* @__PURE__ */ jsx("p", {
2054
+ className: "text-sm text-destructive",
2055
+ children: "Unable to load usage data."
2056
+ }) : null,
2057
+ /* @__PURE__ */ jsxs("div", {
2058
+ className: "mb-5 grid grid-cols-1 gap-4 sm:grid-cols-3",
2059
+ children: [
2060
+ /* @__PURE__ */ jsxs("div", {
2061
+ className: "rounded-xl border border-border bg-card p-4",
2062
+ children: [/* @__PURE__ */ jsx("div", {
2063
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
2064
+ children: "Requests"
2065
+ }), /* @__PURE__ */ jsx("div", {
2066
+ className: "mt-1.5 text-2xl font-bold",
2067
+ children: data ? formatNumber(data.requestCount) : "—"
2068
+ })]
2069
+ }),
2070
+ /* @__PURE__ */ jsxs("div", {
2071
+ className: "rounded-xl border border-border bg-card p-4",
2072
+ children: [/* @__PURE__ */ jsx("div", {
2073
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
2074
+ children: "Avg latency"
2075
+ }), /* @__PURE__ */ jsx("div", {
2076
+ className: "mt-1.5 text-2xl font-bold",
2077
+ children: data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
2078
+ })]
2079
+ }),
2080
+ /* @__PURE__ */ jsxs("div", {
2081
+ className: "rounded-xl border border-border bg-card p-4",
2082
+ children: [/* @__PURE__ */ jsx("div", {
2083
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
2084
+ children: "Error rate"
2085
+ }), /* @__PURE__ */ jsx("div", {
2086
+ className: "mt-1.5 text-2xl font-bold",
2087
+ children: data ? formatPct(data.errorRate) : "—"
2088
+ })]
2089
+ })
2090
+ ]
2091
+ }),
2092
+ /* @__PURE__ */ jsxs("div", {
2093
+ className: "mb-5 rounded-xl border border-border bg-card p-6",
2094
+ children: [/* @__PURE__ */ jsxs("h2", {
2095
+ className: "mb-4 text-lg font-semibold",
2096
+ children: [
2097
+ "Requests over time (",
2098
+ range,
2099
+ ")"
2100
+ ]
2101
+ }), data && data.series.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
2102
+ className: "flex h-40 items-end gap-1.5",
2103
+ children: data.series.map((point) => /* @__PURE__ */ jsx("div", {
2104
+ title: `${point.dayUtc}: ${point.requestCount}`,
2105
+ className: "min-w-0 flex-1 rounded-t-sm bg-primary",
2106
+ style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
2107
+ }, point.dayUtc))
2108
+ }) : /* @__PURE__ */ jsx("p", {
2109
+ className: "text-sm text-muted-foreground",
2110
+ children: "No request data for this range yet."
2111
+ })]
2112
+ }),
2113
+ /* @__PURE__ */ jsxs("div", {
2114
+ className: "grid grid-cols-1 gap-5 lg:grid-cols-[1.2fr_1fr]",
2115
+ children: [/* @__PURE__ */ jsxs("div", {
2116
+ className: "rounded-xl border border-border bg-card p-6",
2117
+ children: [/* @__PURE__ */ jsx("h2", {
2118
+ className: "mb-4 text-lg font-semibold",
2119
+ children: "By endpoint"
2120
+ }), data && data.byEndpoint.length > 0 ? /* @__PURE__ */ jsx("div", {
2121
+ className: "space-y-3.5",
2122
+ children: data.byEndpoint.map((item) => {
2123
+ const pct = endpointTotal > 0 ? Math.round(item.count / endpointTotal * 100) : 0;
2124
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
2125
+ className: "mb-1 flex justify-between text-[13px]",
2126
+ children: [/* @__PURE__ */ jsx("code", {
2127
+ className: "font-mono text-foreground",
2128
+ children: item.path
2129
+ }), /* @__PURE__ */ jsxs("span", {
2130
+ className: "text-muted-foreground",
2131
+ children: [pct, "%"]
2132
+ })]
2133
+ }), /* @__PURE__ */ jsx("div", {
2134
+ className: "h-1.5 rounded-full bg-muted",
2135
+ children: /* @__PURE__ */ jsx("div", {
2136
+ className: "h-full rounded-full bg-primary",
2137
+ style: { width: `${pct}%` }
2138
+ })
2139
+ })] }, item.path);
2140
+ })
2141
+ }) : /* @__PURE__ */ jsx("p", {
2142
+ className: "text-sm text-muted-foreground",
2143
+ children: "No endpoint data."
2144
+ })]
2145
+ }), /* @__PURE__ */ jsxs("div", {
2146
+ className: "rounded-xl border border-border bg-card p-6",
2147
+ children: [/* @__PURE__ */ jsx("h2", {
2148
+ className: "mb-4 text-lg font-semibold",
2149
+ children: "Status codes"
2150
+ }), data && data.byStatus.length > 0 ? /* @__PURE__ */ jsx("div", { children: data.byStatus.map((item) => /* @__PURE__ */ jsxs("div", {
2151
+ className: "flex items-center justify-between border-b border-border py-2.5 last:border-0",
2152
+ children: [/* @__PURE__ */ jsx("span", {
2153
+ className: "text-sm font-medium",
2154
+ children: item.label
2155
+ }), /* @__PURE__ */ jsx("span", {
2156
+ className: "text-sm font-bold",
2157
+ children: formatNumber(item.count)
2158
+ })]
2159
+ }, item.label)) }) : /* @__PURE__ */ jsx("p", {
2160
+ className: "text-sm text-muted-foreground",
2161
+ children: "No status data."
2162
+ })]
2163
+ })]
1501
2164
  })
1502
2165
  ]
1503
2166
  });
@@ -1530,8 +2193,11 @@ function withoutLoginNavigation(items) {
1530
2193
 
1531
2194
  //#endregion
1532
2195
  //#region src/utils/navigation-plans.ts
2196
+ function isPricingLink(item) {
2197
+ return typeof item !== "string" && item.to === PRICING_PATH;
2198
+ }
1533
2199
  function hasPricingLink(items) {
1534
- return items.some((item) => typeof item !== "string" && item.type === "link" && item.to === PRICING_PATH);
2200
+ return items.some(isPricingLink);
1535
2201
  }
1536
2202
  function withPricingNavigation(items) {
1537
2203
  const current = items ?? [];
@@ -1542,11 +2208,26 @@ function withPricingNavigation(items) {
1542
2208
  label: "Pricing"
1543
2209
  }];
1544
2210
  }
2211
+ /** Header nav uses `{ label, to }` (no `type`). Insert before Docs when present. */
2212
+ function withHeaderPricingNavigation(items) {
2213
+ const current = [...items ?? []];
2214
+ if (hasPricingLink(current)) return current;
2215
+ const pricingItem = {
2216
+ label: "Pricing",
2217
+ to: PRICING_PATH
2218
+ };
2219
+ const docsIndex = current.findIndex((item) => typeof item !== "string" && (item.to === "/introduction" || item.label === "Docs"));
2220
+ if (docsIndex >= 0) {
2221
+ current.splice(docsIndex, 0, pricingItem);
2222
+ return current;
2223
+ }
2224
+ return [...current, pricingItem];
2225
+ }
1545
2226
  function withoutPricingNavigation(items) {
1546
2227
  if (!items?.length) return [];
1547
2228
  return items.map((item) => {
1548
2229
  if (typeof item === "string") return item;
1549
- if (item.type === "link" && item.to === PRICING_PATH) return null;
2230
+ if (isPricingLink(item)) return null;
1550
2231
  if (item.items?.length) {
1551
2232
  const filteredItems = withoutPricingNavigation(item.items);
1552
2233
  return {
@@ -1562,7 +2243,7 @@ function withoutPricingNavigation(items) {
1562
2243
  //#region src/utils/header-navigation.ts
1563
2244
  function buildDevPortalHeaderNavigation({ navigation, pricingEnabled, authNavEnabled }) {
1564
2245
  let items = [...navigation ?? []];
1565
- if (!pricingEnabled) items = withoutPricingNavigation(items);
2246
+ items = pricingEnabled ? withHeaderPricingNavigation(items) : withoutPricingNavigation(items);
1566
2247
  if (!authNavEnabled) items = withoutLoginNavigation(items);
1567
2248
  return items;
1568
2249
  }
@@ -1620,21 +2301,36 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
1620
2301
  {
1621
2302
  path: SUBSCRIPTIONS_PATH,
1622
2303
  handle: { layout: "default" },
1623
- element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2304
+ element: /* @__PURE__ */ jsx(AccountLayout, {
2305
+ includeBillingNav: true,
2306
+ children: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2307
+ })
1624
2308
  },
1625
2309
  {
1626
2310
  path: CREDENTIALS_PATH,
1627
2311
  handle: { layout: "default" },
1628
- element: /* @__PURE__ */ jsx(CredentialsPage, {})
2312
+ element: /* @__PURE__ */ jsx(AccountLayout, {
2313
+ includeBillingNav: true,
2314
+ children: /* @__PURE__ */ jsx(CredentialsPage, {})
2315
+ })
2316
+ },
2317
+ {
2318
+ path: USAGE_PATH,
2319
+ handle: { layout: "default" },
2320
+ element: /* @__PURE__ */ jsx(AccountLayout, {
2321
+ includeBillingNav: true,
2322
+ children: /* @__PURE__ */ jsx(UsagePage, {})
2323
+ })
1629
2324
  }
1630
2325
  ]
1631
2326
  }],
1632
2327
  getProtectedRoutes: () => [
1633
2328
  CHECKOUT_PATH,
1634
2329
  SUBSCRIPTIONS_PATH,
1635
- CREDENTIALS_PATH
2330
+ CREDENTIALS_PATH,
2331
+ USAGE_PATH
1636
2332
  ]
1637
2333
  }));
1638
2334
 
1639
2335
  //#endregion
1640
- export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
2336
+ export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, OverviewDashboardPage, PRICING_PATH, SUBSCRIPTIONS_PATH, USAGE_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.1.88",
3
+ "version": "0.1.90",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -46,11 +46,13 @@
46
46
  "react": "19.2.4",
47
47
  "react-dom": "19.2.4",
48
48
  "tsdown": "0.20.3",
49
- "@lukoweb/apitogo": "0.1.88"
49
+ "@lukoweb/apitogo": "0.1.90",
50
+ "@lukoweb/apitogo-module-user-panel": "0.1.90"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "react": ">=19.2.0",
53
54
  "react-dom": ">=19.2.0",
54
- "@lukoweb/apitogo": "*"
55
+ "@lukoweb/apitogo": "*",
56
+ "@lukoweb/apitogo-module-user-panel": "*"
55
57
  }
56
58
  }