@hot-updater/console 0.36.6 → 1.0.0-rc.0

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.
Files changed (266) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +1 -0
  3. package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +42 -0
  4. package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +1 -0
  5. package/.output/public/assets/alert-C-1KvDW8.js +1 -0
  6. package/.output/public/assets/analytics-Bqz0c-BT.js +1 -0
  7. package/.output/public/assets/analytics-api-CJdrTLLO.js +1 -0
  8. package/.output/public/assets/api-B_Rh-uzC.js +1 -0
  9. package/.output/public/assets/api-keys-BurCFhzH.js +1 -0
  10. package/.output/public/assets/api-keys-api-BEDJdQg3.js +1 -0
  11. package/.output/public/assets/badge-Bf0fZ-Gx.js +1 -0
  12. package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
  13. package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
  14. package/.output/public/assets/dialog-BS60BRr2.js +1 -0
  15. package/.output/public/assets/dist-DspNKfMj.js +1 -0
  16. package/.output/public/assets/download-O-yNBc9H.js +1 -0
  17. package/.output/public/assets/field-Buq148-A.js +1 -0
  18. package/.output/public/assets/index-kFhd2S78.js +10 -0
  19. package/.output/public/assets/installations-BrQ-od41.js +2 -0
  20. package/.output/public/assets/installations-CGlZ3ROz.js +1 -0
  21. package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +1 -0
  22. package/.output/public/assets/loader-circle-DCYF-wJl.js +1 -0
  23. package/.output/public/assets/preload-helper-TKOBYGYD.js +1 -0
  24. package/.output/public/assets/refresh-cw-n8XYbPPj.js +1 -0
  25. package/.output/public/assets/routes-CrdokJ80.js +1 -0
  26. package/.output/public/assets/routes-DcQnVajO.js +2 -0
  27. package/.output/public/assets/search-DLhbWw3W.js +1 -0
  28. package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
  29. package/.output/public/assets/signing-HRexfjsH.js +1 -0
  30. package/.output/public/assets/styles-eB4EtvFS.css +2 -0
  31. package/.output/public/assets/table-D8CLieHX.js +1 -0
  32. package/.output/public/assets/textarea-YJLTVAoq.js +1 -0
  33. package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
  34. package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +1 -0
  35. package/.output/server/__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs +146 -0
  36. package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
  37. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  38. package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
  39. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
  40. package/.output/server/_libs/d3-array.mjs +231 -0
  41. package/.output/server/_libs/d3-color.mjs +330 -0
  42. package/.output/server/_libs/d3-format.mjs +252 -0
  43. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  44. package/.output/server/_libs/d3-path.mjs +77 -0
  45. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  46. package/.output/server/_libs/d3-shape.mjs +818 -0
  47. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  48. package/.output/server/_libs/es-toolkit.mjs +1134 -0
  49. package/.output/server/_libs/eventemitter3.mjs +236 -0
  50. package/.output/server/_libs/immer.mjs +532 -0
  51. package/.output/server/_libs/lucide-react.mjs +141 -13
  52. package/.output/server/_libs/next-themes.mjs +2 -2
  53. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  54. package/.output/server/_libs/recharts+[...].mjs +14117 -0
  55. package/.output/server/_libs/sonner.mjs +2 -2
  56. package/.output/server/_libs/tanstack__query-core.mjs +114 -1
  57. package/.output/server/_libs/tanstack__react-query.mjs +3 -4
  58. package/.output/server/_runtime.mjs +10 -1
  59. package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +15 -0
  60. package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +288 -0
  61. package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +13 -0
  62. package/.output/server/_ssr/alert-Dim3MGY5.mjs +86 -0
  63. package/.output/server/_ssr/analytics-DtMTn8fn.mjs +858 -0
  64. package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +71 -0
  65. package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +96 -0
  66. package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +155 -0
  67. package/.output/server/_ssr/api-BTSv1tZb.mjs +282 -0
  68. package/.output/server/_ssr/api-keys-BnM_o2nS.mjs +300 -0
  69. package/.output/server/_ssr/api-keys-api-CIywsSF5.mjs +60 -0
  70. package/.output/server/_ssr/api-keys-rpc-BSp1wkIV.mjs +136 -0
  71. package/.output/server/_ssr/api-rpc-cG5wd_tO.mjs +1468 -0
  72. package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
  73. package/.output/server/_ssr/auth-rpc-BrJ6obHJ.mjs +24 -0
  74. package/.output/server/_ssr/auth.server-DwKoZoQM.mjs +50 -0
  75. package/.output/server/_ssr/badge-DCxbEeof.mjs +26 -0
  76. package/.output/server/_ssr/bundle-signing-rpc-COoQhW8E.mjs +16 -0
  77. package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +51 -0
  78. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
  79. package/.output/server/_ssr/config.server-DEiLhKmy.mjs +361 -0
  80. package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
  81. package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
  82. package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-9P0S0lJ3.mjs} +68 -50
  83. package/.output/server/_ssr/databaseRows-CwbNwYSl.mjs +166 -0
  84. package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
  85. package/.output/server/_ssr/dialog-CQuRMNZ4.mjs +170 -0
  86. package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
  87. package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
  88. package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
  89. package/.output/server/_ssr/field-DRIXKjZG.mjs +74 -0
  90. package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
  91. package/.output/server/_ssr/installations-B-azgAvX.mjs +19 -0
  92. package/.output/server/_ssr/installations-Ctvzp2JH.mjs +535 -0
  93. package/.output/server/_ssr/router-C-KUBfOi.mjs +513 -0
  94. package/.output/server/_ssr/routes-BM4ht9jb.mjs +2286 -0
  95. package/.output/server/_ssr/routes-Z7TwC6nC.mjs +37 -0
  96. package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +527 -0
  97. package/.output/server/_ssr/server-BjzmvXnO.mjs +7468 -0
  98. package/.output/server/_ssr/signing-k8GxyBDF.mjs +202 -0
  99. package/.output/server/_ssr/ssr.mjs +4 -5740
  100. package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
  101. package/.output/server/_ssr/table-v1yCvYo7.mjs +53 -0
  102. package/.output/server/_ssr/textarea-DYHnskhb.mjs +14 -0
  103. package/.output/server/_ssr/uuidv7-BbJ-9D_I.mjs +31 -0
  104. package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +108 -0
  105. package/.output/server/index.mjs +249 -46
  106. package/README.md +29 -8
  107. package/lib/index.d.mts +36 -0
  108. package/lib/index.mjs +4 -0
  109. package/lib/vite-internal.mjs +127 -0
  110. package/lib/vite.d.mts +10 -0
  111. package/lib/vite.mjs +5 -0
  112. package/package.json +51 -29
  113. package/public/apple-touch-icon.png +0 -0
  114. package/public/favicon-16x16.png +0 -0
  115. package/public/favicon-32x32.png +0 -0
  116. package/public/favicon.ico +0 -0
  117. package/public/logo.svg +1 -0
  118. package/public/manifest.json +30 -0
  119. package/public/robots.txt +3 -0
  120. package/src/components/AppSidebar.tsx +159 -0
  121. package/src/components/BundleIdDisplay.tsx +18 -0
  122. package/src/components/ChannelBadge.tsx +31 -0
  123. package/src/components/ConsoleAccessPage.tsx +122 -0
  124. package/src/components/EnabledStatusIcon.tsx +54 -0
  125. package/src/components/HashValueDisplay.tsx +85 -0
  126. package/src/components/HotUpdaterLogo.tsx +30 -0
  127. package/src/components/NotFoundPage.tsx +33 -0
  128. package/src/components/PlatformIcon.tsx +39 -0
  129. package/src/components/RolloutPercentageBadge.tsx +28 -0
  130. package/src/components/ThemeProvider.tsx +116 -0
  131. package/src/components/TimestampDisplay.tsx +41 -0
  132. package/src/components/features/analytics/ActivityChart.tsx +150 -0
  133. package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +24 -0
  134. package/src/components/features/analytics/AnalyticsControls.tsx +115 -0
  135. package/src/components/features/analytics/AnalyticsErrorAlert.tsx +21 -0
  136. package/src/components/features/analytics/AnalyticsOverview.tsx +208 -0
  137. package/src/components/features/analytics/BundleDistribution.tsx +133 -0
  138. package/src/components/features/analytics/BundleSelector.tsx +114 -0
  139. package/src/components/features/analytics/InstallationHistoryCard.tsx +248 -0
  140. package/src/components/features/analytics/InstallationMatchesCard.tsx +132 -0
  141. package/src/components/features/analytics/InstallationPageHeader.tsx +138 -0
  142. package/src/components/features/analytics/InstallationPagination.tsx +48 -0
  143. package/src/components/features/analytics/InstallationSearch.tsx +230 -0
  144. package/src/components/features/analytics/UpdateOutcomes.tsx +147 -0
  145. package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
  146. package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
  147. package/src/components/features/bundles/BundleAnalyticsSummary.tsx +123 -0
  148. package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
  149. package/src/components/features/bundles/BundleMetadata.tsx +136 -0
  150. package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
  151. package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
  152. package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
  153. package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
  154. package/src/components/features/signing/BundleSigningPage.tsx +247 -0
  155. package/src/components/ui/alert-dialog.tsx +185 -0
  156. package/src/components/ui/alert.tsx +76 -0
  157. package/src/components/ui/badge.tsx +49 -0
  158. package/src/components/ui/button.tsx +58 -0
  159. package/src/components/ui/card.tsx +89 -0
  160. package/src/components/ui/chart.tsx +371 -0
  161. package/src/components/ui/combobox.tsx +303 -0
  162. package/src/components/ui/dialog.tsx +156 -0
  163. package/src/components/ui/dropdown-menu.tsx +270 -0
  164. package/src/components/ui/field.tsx +235 -0
  165. package/src/components/ui/input-group.tsx +147 -0
  166. package/src/components/ui/input.tsx +19 -0
  167. package/src/components/ui/label.tsx +25 -0
  168. package/src/components/ui/progress.tsx +84 -0
  169. package/src/components/ui/select.tsx +205 -0
  170. package/src/components/ui/separator.tsx +23 -0
  171. package/src/components/ui/sheet.tsx +132 -0
  172. package/src/components/ui/sidebar.tsx +779 -0
  173. package/src/components/ui/skeleton.tsx +13 -0
  174. package/src/components/ui/slider.tsx +56 -0
  175. package/src/components/ui/sonner.tsx +45 -0
  176. package/src/components/ui/switch.tsx +25 -0
  177. package/src/components/ui/table.tsx +114 -0
  178. package/src/components/ui/textarea.tsx +18 -0
  179. package/src/components/ui/toggle-group.tsx +89 -0
  180. package/src/components/ui/toggle.tsx +43 -0
  181. package/src/components/ui/tooltip.tsx +66 -0
  182. package/src/hooks/use-mobile.ts +30 -0
  183. package/src/index.ts +48 -0
  184. package/src/lib/analytics-api.ts +145 -0
  185. package/src/lib/analytics-error.ts +19 -0
  186. package/src/lib/analytics-input.ts +187 -0
  187. package/src/lib/analytics-overview.ts +170 -0
  188. package/src/lib/analytics-rpc.ts +218 -0
  189. package/src/lib/api-keys-api.ts +69 -0
  190. package/src/lib/api-keys-rpc.ts +87 -0
  191. package/src/lib/api-rpc.ts +478 -0
  192. package/src/lib/api.ts +444 -0
  193. package/src/lib/auth-rpc.ts +16 -0
  194. package/src/lib/bundle-signing-api.ts +14 -0
  195. package/src/lib/bundle-signing-rpc.ts +16 -0
  196. package/src/lib/constants.ts +1 -0
  197. package/src/lib/git.ts +51 -0
  198. package/src/lib/server/auth.server.ts +43 -0
  199. package/src/lib/server/bundle-signing.server.ts +100 -0
  200. package/src/lib/server/config.server.ts +109 -0
  201. package/src/lib/server/console-runtime.server.ts +70 -0
  202. package/src/lib/server/deleteBundle.ts +145 -0
  203. package/src/lib/server/downloadBundle.ts +46 -0
  204. package/src/lib/server/getBundleChildren.ts +130 -0
  205. package/src/lib/server/listReleases.ts +61 -0
  206. package/src/lib/server/releaseActivity.ts +27 -0
  207. package/src/lib/server/releaseReachability.ts +95 -0
  208. package/src/lib/server/runtime.server.ts +172 -0
  209. package/src/lib/utils.ts +9 -0
  210. package/src/logo.svg +1 -0
  211. package/src/routeTree.gen.ts +199 -0
  212. package/src/router.tsx +21 -0
  213. package/src/routes/-installations-search.ts +19 -0
  214. package/src/routes/-releases-search.ts +74 -0
  215. package/src/routes/__root.tsx +174 -0
  216. package/src/routes/analytics.tsx +139 -0
  217. package/src/routes/api/auth/$.ts +19 -0
  218. package/src/routes/api/bundles/$bundleId/download.ts +17 -0
  219. package/src/routes/api-keys.tsx +37 -0
  220. package/src/routes/index.tsx +982 -0
  221. package/src/routes/installations.tsx +194 -0
  222. package/src/routes/signing.tsx +37 -0
  223. package/src/styles.css +152 -0
  224. package/src/virtual-modules.d.ts +21 -0
  225. package/src/vite-env.d.ts +1 -0
  226. package/src/vite-internal.ts +195 -0
  227. package/src/vite.ts +12 -0
  228. package/.output/public/assets/dist-DJX53faP.js +0 -3
  229. package/.output/public/assets/index-Iylis5kY.js +0 -10
  230. package/.output/public/assets/routes-HT21hs0C.js +0 -54
  231. package/.output/public/assets/styles-C0Re4quB.css +0 -2
  232. package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
  233. package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
  234. package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
  235. package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
  236. package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
  237. package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
  238. package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
  239. package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
  240. package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
  241. package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
  242. package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
  243. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  244. package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
  245. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  246. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
  247. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
  248. package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
  249. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
  250. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
  251. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
  252. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
  253. package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
  254. package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
  255. package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
  256. package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
  257. package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
  258. package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
  259. package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
  260. package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
  261. package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
  262. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  263. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  264. package/.output/server/node_modules/tslib/package.json +0 -47
  265. package/.output/server/node_modules/tslib/tslib.js +0 -484
  266. package/.output/server/package.json +0 -9
@@ -0,0 +1,31 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ import { Badge } from "./ui/badge";
4
+
5
+ interface ChannelBadgeProps {
6
+ channel: string;
7
+ className?: string;
8
+ }
9
+
10
+ const channelColors: Record<string, string> = {
11
+ production:
12
+ "bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20",
13
+ dev: "bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20",
14
+ staging:
15
+ "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 border-yellow-500/20",
16
+ };
17
+
18
+ export function ChannelBadge({ channel, className }: ChannelBadgeProps) {
19
+ return (
20
+ <Badge
21
+ variant="outline"
22
+ className={cn(
23
+ channelColors[channel.toLowerCase()] ||
24
+ "bg-gray-500/10 text-gray-700 dark:text-gray-400 border-gray-500/20",
25
+ className,
26
+ )}
27
+ >
28
+ {channel}
29
+ </Badge>
30
+ );
31
+ }
@@ -0,0 +1,122 @@
1
+ import { useState } from "react";
2
+
3
+ import { HotUpdaterLogo } from "@/components/HotUpdaterLogo";
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ import type { ConsoleAccess, ConsoleAuthProvider } from "../index";
7
+
8
+ type ConsoleAccessPageProps = {
9
+ readonly access: Exclude<ConsoleAccess, { status: "authorized" }>;
10
+ readonly providers: readonly ConsoleAuthProvider[];
11
+ };
12
+
13
+ const providerLabel: Record<ConsoleAuthProvider, string> = {
14
+ github: "GitHub",
15
+ google: "Google",
16
+ };
17
+
18
+ export function ConsoleAccessPage({
19
+ access,
20
+ providers,
21
+ }: ConsoleAccessPageProps) {
22
+ const [pendingProvider, setPendingProvider] =
23
+ useState<ConsoleAuthProvider | null>(null);
24
+ const [signingOut, setSigningOut] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+
27
+ const signIn = async (provider: ConsoleAuthProvider) => {
28
+ setPendingProvider(provider);
29
+ setError(null);
30
+ try {
31
+ const response = await fetch("/api/auth/sign-in/social", {
32
+ body: JSON.stringify({ callbackURL: "/", provider }),
33
+ headers: { "content-type": "application/json" },
34
+ method: "POST",
35
+ });
36
+ const result = (await response.json()) as { readonly url?: unknown };
37
+ if (!response.ok || typeof result.url !== "string") {
38
+ throw new Error("The identity provider did not return a sign-in URL.");
39
+ }
40
+ window.location.assign(result.url);
41
+ } catch (cause) {
42
+ setPendingProvider(null);
43
+ setError(cause instanceof Error ? cause.message : "Sign-in failed.");
44
+ }
45
+ };
46
+
47
+ const signOut = async () => {
48
+ setSigningOut(true);
49
+ setError(null);
50
+ try {
51
+ const response = await fetch("/api/auth/sign-out", { method: "POST" });
52
+ if (!response.ok) {
53
+ throw new Error("Sign-out failed.");
54
+ }
55
+ window.location.reload();
56
+ } catch (cause) {
57
+ setSigningOut(false);
58
+ setError(cause instanceof Error ? cause.message : "Sign-out failed.");
59
+ }
60
+ };
61
+
62
+ return (
63
+ <main className="bg-background text-foreground flex min-h-screen items-center justify-center p-6">
64
+ <section className="border-border bg-card w-full max-w-sm rounded-xl border p-6 shadow-sm">
65
+ <div className="mb-6 flex items-center gap-3">
66
+ <HotUpdaterLogo className="h-10 w-7" />
67
+ <div>
68
+ <h1 className="text-lg font-semibold">Hot Updater Console</h1>
69
+ <p className="text-muted-foreground text-sm">
70
+ Sign in to manage OTA releases.
71
+ </p>
72
+ </div>
73
+ </div>
74
+
75
+ {access.status === "forbidden" ? (
76
+ <div className="space-y-3">
77
+ <p className="text-sm font-medium">Access denied</p>
78
+ <p className="text-muted-foreground text-sm">
79
+ {access.principal.email} is not in the console allowlist.
80
+ </p>
81
+ <Button
82
+ className="w-full"
83
+ disabled={signingOut}
84
+ onClick={() => void signOut()}
85
+ size="lg"
86
+ variant="outline"
87
+ >
88
+ {signingOut ? "Signing out…" : "Sign out"}
89
+ </Button>
90
+ </div>
91
+ ) : providers.length === 0 ? (
92
+ <p className="text-muted-foreground text-sm">
93
+ No OAuth provider is configured for this console.
94
+ </p>
95
+ ) : (
96
+ <div className="grid gap-2">
97
+ {providers.map((provider) => (
98
+ <Button
99
+ className="w-full"
100
+ disabled={pendingProvider !== null}
101
+ key={provider}
102
+ onClick={() => void signIn(provider)}
103
+ size="lg"
104
+ variant="outline"
105
+ >
106
+ {pendingProvider === provider
107
+ ? "Redirecting…"
108
+ : `Continue with ${providerLabel[provider]}`}
109
+ </Button>
110
+ ))}
111
+ </div>
112
+ )}
113
+
114
+ {error ? (
115
+ <p className="text-destructive mt-3 text-sm" role="alert">
116
+ {error}
117
+ </p>
118
+ ) : null}
119
+ </section>
120
+ </main>
121
+ );
122
+ }
@@ -0,0 +1,54 @@
1
+ import { Check, Minus, X } from "lucide-react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface EnabledStatusIconProps {
6
+ enabled: boolean;
7
+ className?: string;
8
+ falseIcon?: "minus" | "x";
9
+ colorMode?: "semantic" | "inherit";
10
+ }
11
+
12
+ export function EnabledStatusIcon({
13
+ enabled,
14
+ className,
15
+ falseIcon = "x",
16
+ colorMode = "semantic",
17
+ }: EnabledStatusIconProps) {
18
+ const iconColorClassName =
19
+ colorMode === "inherit" ? "text-current" : undefined;
20
+
21
+ if (enabled) {
22
+ return (
23
+ <Check
24
+ className={cn(
25
+ "h-4 w-4",
26
+ iconColorClassName ?? "text-green-600 dark:text-green-400",
27
+ className,
28
+ )}
29
+ />
30
+ );
31
+ }
32
+
33
+ if (falseIcon === "minus") {
34
+ return (
35
+ <Minus
36
+ className={cn(
37
+ "h-4 w-4",
38
+ iconColorClassName ?? "text-muted-foreground",
39
+ className,
40
+ )}
41
+ />
42
+ );
43
+ }
44
+
45
+ return (
46
+ <X
47
+ className={cn(
48
+ "h-4 w-4",
49
+ iconColorClassName ?? "text-red-600 dark:text-red-400",
50
+ className,
51
+ )}
52
+ />
53
+ );
54
+ }
@@ -0,0 +1,85 @@
1
+ import { toast } from "sonner";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
6
+
7
+ interface HashValueDisplayProps {
8
+ value: string;
9
+ maxLength?: number;
10
+ className?: string;
11
+ }
12
+
13
+ export function HashValueDisplay({
14
+ value,
15
+ maxLength = 12,
16
+ className,
17
+ }: HashValueDisplayProps) {
18
+ const isTruncated = value.length > maxLength;
19
+ const truncated = isTruncated ? value.slice(0, maxLength) : value;
20
+
21
+ const copyValue = async () => {
22
+ try {
23
+ await navigator.clipboard.writeText(value);
24
+ toast.success("Copied to clipboard", {
25
+ description: value,
26
+ });
27
+ } catch {
28
+ toast.error("Failed to copy value");
29
+ }
30
+ };
31
+
32
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
33
+ event.preventDefault();
34
+ event.stopPropagation();
35
+ void copyValue();
36
+ };
37
+
38
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
39
+ if (event.key === "Enter" || event.key === " ") {
40
+ event.preventDefault();
41
+ event.stopPropagation();
42
+ void copyValue();
43
+ }
44
+ };
45
+
46
+ const content = (
47
+ <span
48
+ translate="no"
49
+ className={cn(
50
+ "font-mono text-xs tabular-nums whitespace-nowrap",
51
+ className,
52
+ )}
53
+ >
54
+ {truncated}
55
+ </span>
56
+ );
57
+
58
+ return (
59
+ <Tooltip>
60
+ <TooltipTrigger
61
+ render={
62
+ <button
63
+ type="button"
64
+ translate="no"
65
+ className={cn(
66
+ "ring-ring/30 bg-muted/40 border-border/70 inline-flex min-w-0 max-w-full cursor-pointer items-center rounded-md border px-1.5 py-0.5 align-top shadow-xs outline-none transition-[background-color,border-color,transform,box-shadow]",
67
+ "hover:bg-muted/70 active:scale-[0.98] active:bg-muted/85 focus-visible:ring-[2px]",
68
+ "touch-manipulation select-none",
69
+ !isTruncated && "cursor-copy",
70
+ )}
71
+ onClick={handleClick}
72
+ onKeyDown={handleKeyDown}
73
+ />
74
+ }
75
+ >
76
+ {content}
77
+ </TooltipTrigger>
78
+ <TooltipContent hidden={!isTruncated}>
79
+ <p translate="no" className="break-all font-mono text-xs tabular-nums">
80
+ {value}
81
+ </p>
82
+ </TooltipContent>
83
+ </Tooltip>
84
+ );
85
+ }
@@ -0,0 +1,30 @@
1
+ export function HotUpdaterLogo({ className }: { className?: string }) {
2
+ return (
3
+ <svg
4
+ aria-label="Hot Updater"
5
+ className={className}
6
+ viewBox="0 0 447 652"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ >
9
+ <defs>
10
+ <linearGradient
11
+ id="hot-updater-gradient"
12
+ x1="223.54"
13
+ y1="0.36"
14
+ x2="223.54"
15
+ y2="711.23"
16
+ gradientUnits="userSpaceOnUse"
17
+ >
18
+ <stop offset="0" stopColor="#ff7f33" />
19
+ <stop offset="0.4" stopColor="#f9602b" />
20
+ <stop offset="0.76" stopColor="#f54c25" />
21
+ <stop offset="1" stopColor="#f44523" />
22
+ </linearGradient>
23
+ </defs>
24
+ <path
25
+ d="M150.25,8.58c16.28,31.85,42.09,116-68.18,212.22C75.46,226.57,70,233,63.72,239.62c-28.48,30-84.71,114.89-55.82,207,.69,2.18,1.31,4.37,1.91,6.57,5.25,19.33,38.08,121,155.56,157.25a5.14,5.14,0,0,0,6-7.39c-14.72-26.53-46.07-96.89-8.89-162.55,0,0,11.5,44.5,50.5,83.5,6,6,14.72,15.64,19.53,21.23,14.47,16.77,32.24,53.55,1.82,98.18a5.63,5.63,0,0,0,6.85,8.34c75.64-32.1,333.29-164.6,128.68-410.85a7,7,0,0,0-12.46,4.15c-1.77,39.31-14.84,129-96.18,148.06a31.08,31.08,0,0,1-24.3-4.16c-13.34-8.9-25.41-32.06.53-92.3a250.9,250.9,0,0,1,20.12-37.29C282.78,220.56,356.18,79.83,157.33.77A5.64,5.64,0,0,0,150.25,8.58Z"
26
+ fill="url(#hot-updater-gradient)"
27
+ />
28
+ </svg>
29
+ );
30
+ }
@@ -0,0 +1,33 @@
1
+ import { Link } from "@tanstack/react-router";
2
+
3
+ import { buttonVariants } from "@/components/ui/button";
4
+
5
+ const homeSearch = {
6
+ channel: undefined,
7
+ platform: undefined,
8
+ targetAppVersion: undefined,
9
+ page: undefined,
10
+ after: undefined,
11
+ before: undefined,
12
+ bundleId: undefined,
13
+ expandedBundleId: undefined,
14
+ } as const;
15
+
16
+ export function NotFoundPage() {
17
+ return (
18
+ <div className="flex min-h-svh flex-1 items-center justify-center px-6 py-10">
19
+ <section className="flex max-w-md flex-col items-center text-center">
20
+ <h1 className="text-2xl font-semibold tracking-tight text-foreground">
21
+ Page not found
22
+ </h1>
23
+ <Link
24
+ className={buttonVariants({ className: "mt-6", size: "lg" })}
25
+ to="/"
26
+ search={homeSearch}
27
+ >
28
+ Go to home
29
+ </Link>
30
+ </section>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,39 @@
1
+ interface PlatformIconProps {
2
+ platform: "ios" | "android";
3
+ className?: string;
4
+ }
5
+
6
+ function AppleIcon({ className }: { className?: string }) {
7
+ return (
8
+ <svg
9
+ aria-hidden="true"
10
+ viewBox="0 0 24 24"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className}
13
+ fill="currentColor"
14
+ >
15
+ <path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
16
+ </svg>
17
+ );
18
+ }
19
+
20
+ function AndroidIcon({ className }: { className?: string }) {
21
+ return (
22
+ <svg
23
+ aria-hidden="true"
24
+ viewBox="0 0 24 24"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ className={className}
27
+ fill="currentColor"
28
+ >
29
+ <path d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z" />
30
+ </svg>
31
+ );
32
+ }
33
+
34
+ export function PlatformIcon({ platform, className }: PlatformIconProps) {
35
+ if (platform === "ios") {
36
+ return <AppleIcon className={className} />;
37
+ }
38
+ return <AndroidIcon className={className} />;
39
+ }
@@ -0,0 +1,28 @@
1
+ import { AlertTriangle } from "lucide-react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ import { Badge } from "./ui/badge";
6
+
7
+ interface RolloutPercentageBadgeProps {
8
+ percentage: number;
9
+ className?: string;
10
+ }
11
+
12
+ export function RolloutPercentageBadge({
13
+ percentage,
14
+ className,
15
+ }: RolloutPercentageBadgeProps) {
16
+ const isPartialRollout = percentage < 100;
17
+ const formattedPercentage = percentage.toFixed(1);
18
+
19
+ return (
20
+ <Badge
21
+ className={cn("gap-1", className)}
22
+ variant={isPartialRollout ? "secondary" : "default"}
23
+ >
24
+ {isPartialRollout ? <AlertTriangle className="h-3 w-3" /> : null}
25
+ {formattedPercentage}%
26
+ </Badge>
27
+ );
28
+ }
@@ -0,0 +1,116 @@
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
+
3
+ type Theme = "dark" | "light" | "system";
4
+
5
+ interface ThemeContextType {
6
+ theme: Theme;
7
+ setTheme: (theme: Theme) => void;
8
+ }
9
+
10
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
11
+ const LIGHT_THEME_COLOR = "#fbfbfa";
12
+ const DARK_THEME_COLOR = "#1f1d1c";
13
+ const REFRESH_THEME_CHROME_EVENT = "hot-updater:refresh-theme-chrome";
14
+
15
+ export function ThemeProvider({
16
+ children,
17
+ defaultTheme = "dark",
18
+ storageKey = "hot-updater-theme",
19
+ }: {
20
+ children: React.ReactNode;
21
+ defaultTheme?: Theme;
22
+ storageKey?: string;
23
+ }) {
24
+ const [theme, setTheme] = useState<Theme>(defaultTheme);
25
+
26
+ useEffect(() => {
27
+ const stored = localStorage.getItem(storageKey) as Theme | null;
28
+ if (stored) {
29
+ setTheme(stored);
30
+ }
31
+ }, [storageKey]);
32
+
33
+ useEffect(() => {
34
+ const root = window.document.documentElement;
35
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
36
+ const themeColorMeta = window.document.querySelector(
37
+ 'meta[name="theme-color"]',
38
+ );
39
+ const colorSchemeMeta = window.document.querySelector(
40
+ 'meta[name="color-scheme"]',
41
+ );
42
+
43
+ const applyResolvedTheme = (resolvedTheme: "dark" | "light") => {
44
+ root.classList.remove("light", "dark");
45
+ root.classList.add(resolvedTheme);
46
+ root.style.colorScheme = resolvedTheme;
47
+
48
+ if (themeColorMeta) {
49
+ themeColorMeta.setAttribute(
50
+ "content",
51
+ resolvedTheme === "dark" ? DARK_THEME_COLOR : LIGHT_THEME_COLOR,
52
+ );
53
+ }
54
+
55
+ if (colorSchemeMeta) {
56
+ colorSchemeMeta.setAttribute(
57
+ "content",
58
+ resolvedTheme === "dark" ? "dark light" : "light dark",
59
+ );
60
+ }
61
+ };
62
+
63
+ const syncTheme = () => {
64
+ const resolvedTheme =
65
+ theme === "system" ? (mediaQuery.matches ? "dark" : "light") : theme;
66
+
67
+ applyResolvedTheme(resolvedTheme);
68
+ };
69
+
70
+ syncTheme();
71
+
72
+ const refreshThemeChrome = () => {
73
+ window.requestAnimationFrame(syncTheme);
74
+ };
75
+
76
+ window.addEventListener(REFRESH_THEME_CHROME_EVENT, refreshThemeChrome);
77
+ window.addEventListener("pageshow", refreshThemeChrome);
78
+ document.addEventListener("visibilitychange", refreshThemeChrome);
79
+
80
+ if (theme === "system") {
81
+ mediaQuery.addEventListener("change", syncTheme);
82
+ }
83
+
84
+ return () => {
85
+ if (theme === "system") {
86
+ mediaQuery.removeEventListener("change", syncTheme);
87
+ }
88
+ window.removeEventListener(
89
+ REFRESH_THEME_CHROME_EVENT,
90
+ refreshThemeChrome,
91
+ );
92
+ window.removeEventListener("pageshow", refreshThemeChrome);
93
+ document.removeEventListener("visibilitychange", refreshThemeChrome);
94
+ };
95
+ }, [theme]);
96
+
97
+ const value = {
98
+ theme,
99
+ setTheme: (newTheme: Theme) => {
100
+ localStorage.setItem(storageKey, newTheme);
101
+ setTheme(newTheme);
102
+ },
103
+ };
104
+
105
+ return (
106
+ <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
107
+ );
108
+ }
109
+
110
+ export function useTheme() {
111
+ const context = useContext(ThemeContext);
112
+ if (context === undefined) {
113
+ throw new Error("useTheme must be used within a ThemeProvider");
114
+ }
115
+ return context;
116
+ }
@@ -0,0 +1,41 @@
1
+ import { extractTimestampFromUUIDv7 } from "@hot-updater/plugin-core";
2
+ import dayjs from "dayjs";
3
+ import relativeTime from "dayjs/plugin/relativeTime";
4
+
5
+ dayjs.extend(relativeTime);
6
+
7
+ interface TimestampDisplayProps {
8
+ uuid: string;
9
+ format?: "relative" | "absolute" | "both";
10
+ }
11
+
12
+ export function TimestampDisplay({
13
+ uuid,
14
+ format = "relative",
15
+ }: TimestampDisplayProps) {
16
+ const timestamp = extractTimestampFromUUIDv7(uuid);
17
+ const date = dayjs(timestamp);
18
+
19
+ if (format === "relative") {
20
+ return (
21
+ <span className="text-sm text-muted-foreground">{date.fromNow()}</span>
22
+ );
23
+ }
24
+
25
+ if (format === "absolute") {
26
+ return (
27
+ <span className="text-sm text-muted-foreground">
28
+ {date.format("YYYY-MM-DD HH:mm:ss")}
29
+ </span>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <div className="flex flex-col">
35
+ <span className="text-sm">{date.fromNow()}</span>
36
+ <span className="text-xs text-muted-foreground">
37
+ {date.format("YYYY-MM-DD HH:mm:ss")}
38
+ </span>
39
+ </div>
40
+ );
41
+ }