@tscircuit/fake-snippets 0.0.81 → 0.0.83

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 (37) hide show
  1. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +3 -3
  2. package/dist/bundle.js +41 -13
  3. package/dist/index.d.ts +24 -4
  4. package/dist/index.js +5 -1
  5. package/dist/schema.d.ts +37 -5
  6. package/dist/schema.js +5 -1
  7. package/fake-snippets-api/lib/db/schema.ts +5 -1
  8. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +14 -1
  9. package/fake-snippets-api/routes/api/package_releases/get.ts +11 -3
  10. package/fake-snippets-api/routes/api/package_releases/list.ts +8 -1
  11. package/fake-snippets-api/routes/api/packages/generate_from_jlcpcb.ts +3 -3
  12. package/package.json +1 -1
  13. package/src/App.tsx +0 -2
  14. package/src/components/JLCPCBImportDialog.tsx +164 -62
  15. package/src/components/PackageBuildsPage/LogContent.tsx +12 -5
  16. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +8 -7
  17. package/src/components/PackageBuildsPage/build-preview-content.tsx +1 -1
  18. package/src/components/PackageBuildsPage/collapsible-section.tsx +14 -46
  19. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +28 -10
  20. package/src/components/PackageBuildsPage/package-build-header.tsx +16 -4
  21. package/src/components/ViewPackagePage/components/build-status.tsx +24 -85
  22. package/src/components/ViewPackagePage/components/important-files-view.tsx +8 -1
  23. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +28 -5
  24. package/src/components/ViewPackagePage/hooks/use-toast.tsx +70 -0
  25. package/src/components/dialogs/{import-snippet-dialog.tsx → import-package-dialog.tsx} +25 -24
  26. package/src/components/package-port/CodeEditor.tsx +9 -1
  27. package/src/components/package-port/CodeEditorHeader.tsx +7 -6
  28. package/src/components/ui/toaster.tsx +1 -33
  29. package/src/hooks/use-current-package-release.ts +14 -3
  30. package/src/hooks/use-now.ts +12 -0
  31. package/src/hooks/use-package-release.ts +17 -15
  32. package/src/hooks/use-toast.tsx +50 -169
  33. package/src/pages/dashboard.tsx +3 -1
  34. package/src/pages/user-profile.tsx +9 -2
  35. package/src/pages/view-package.tsx +1 -0
  36. package/.github/workflows/formatbot.yml +0 -63
  37. package/src/components/ViewPackagePage/hooks/use-toast.ts +0 -191
@@ -20,43 +20,102 @@ interface JLCPCBImportDialogProps {
20
20
  onOpenChange: (open: boolean) => void
21
21
  }
22
22
 
23
- export function JLCPCBImportDialog({
24
- open,
25
- onOpenChange,
26
- }: JLCPCBImportDialogProps) {
27
- const [partNumber, setPartNumber] = useState("")
28
- const [isLoading, setIsLoading] = useState(false)
29
- const [error, setError] = useState<string | null>(null)
30
- const [hasBeenImportedToAccountAlready, setHasBeenImportedToAccountAlready] =
31
- useState<boolean>(false)
23
+ interface ImportState {
24
+ isLoading: boolean
25
+ error: string | null
26
+ existingComponent: {
27
+ partNumber: string
28
+ username: string
29
+ } | null
30
+ }
31
+
32
+ interface JLCPCBResponse {
33
+ ok: boolean
34
+ package: {
35
+ package_id: string
36
+ }
37
+ }
38
+
39
+ interface APIError {
40
+ status: number
41
+ data?: {
42
+ message?: string
43
+ existing_part_number?: string
44
+ part_number?: string
45
+ error?: {
46
+ message?: string
47
+ }
48
+ }
49
+ }
50
+
51
+ const extractErrorMessage = (error: APIError): string => {
52
+ return (
53
+ error?.data?.message ||
54
+ error?.data?.error?.message ||
55
+ "An unexpected error occurred"
56
+ )
57
+ }
58
+
59
+ const extractExistingPartNumber = (
60
+ error: APIError,
61
+ fallback: string,
62
+ ): string => {
63
+ return error?.data?.message || error?.data?.error?.message || fallback
64
+ }
65
+
66
+ const useJLCPCBImport = () => {
67
+ const [state, setState] = useState<ImportState>({
68
+ isLoading: false,
69
+ error: null,
70
+ existingComponent: null,
71
+ })
72
+
32
73
  const axios = useAxios()
33
74
  const { toast } = useToast()
34
75
  const [, navigate] = useLocation()
35
- const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
36
76
  const session = useGlobalStore((s) => s.session)
37
77
 
38
- const handleImport = async () => {
39
- if (!partNumber.startsWith("C")) {
78
+ const resetState = () => {
79
+ setState({
80
+ isLoading: false,
81
+ error: null,
82
+ existingComponent: null,
83
+ })
84
+ }
85
+
86
+ const importComponent = async (partNumber: string) => {
87
+ if (!partNumber.startsWith("C") || partNumber.length < 2) {
40
88
  toast({
41
89
  title: "Invalid Part Number",
42
- description: "JLCPCB part numbers should start with 'C'.",
90
+ description:
91
+ "JLCPCB part numbers should start with 'C' and be at least 2 characters long.",
43
92
  variant: "destructive",
44
93
  })
45
- return
94
+ return { success: false }
46
95
  }
47
96
 
48
- setIsLoading(true)
49
- setError(null)
50
- setHasBeenImportedToAccountAlready(false)
97
+ setState((prev) => ({
98
+ ...prev,
99
+ isLoading: true,
100
+ error: null,
101
+ existingComponent: null,
102
+ }))
51
103
 
52
104
  try {
53
- const response = await axios.post("/packages/generate_from_jlcpcb", {
54
- jlcpcb_part_number: partNumber,
55
- })
105
+ const response = await axios.post<JLCPCBResponse>(
106
+ "/packages/generate_from_jlcpcb",
107
+ {
108
+ jlcpcb_part_number: partNumber,
109
+ },
110
+ )
111
+
56
112
  if (!response.data.ok) {
57
- setError("Failed to generate package from JLCPCB part")
58
- setIsLoading(false)
59
- return
113
+ setState((prev) => ({
114
+ ...prev,
115
+ isLoading: false,
116
+ error: "Failed to generate package from JLCPCB part",
117
+ }))
118
+ return { success: false }
60
119
  }
61
120
 
62
121
  const { package: generatedPackage } = response.data
@@ -66,27 +125,84 @@ export function JLCPCBImportDialog({
66
125
  description: "JLCPCB component has been imported successfully.",
67
126
  })
68
127
 
69
- onOpenChange(false)
70
128
  navigate(`/editor?package_id=${generatedPackage.package_id}`)
129
+ return { success: true }
71
130
  } catch (error: any) {
72
- if (error.response?.status === 404) {
73
- setError(`Component with JLCPCB part number ${partNumber} not found`)
74
- } else if (error.response?.data?.message) {
75
- setError(error.response.data.message)
131
+ const apiError = error as APIError
132
+
133
+ if (apiError.status === 404) {
134
+ setState((prev) => ({
135
+ ...prev,
136
+ isLoading: false,
137
+ error: `Component with JLCPCB part number ${partNumber} not found`,
138
+ }))
139
+ } else if (apiError.status === 409) {
140
+ const existingPartNumber = extractExistingPartNumber(
141
+ apiError,
142
+ partNumber,
143
+ )
144
+ setState((prev) => ({
145
+ ...prev,
146
+ isLoading: false,
147
+ existingComponent: {
148
+ partNumber: existingPartNumber,
149
+ username: session?.github_username || "",
150
+ },
151
+ }))
76
152
  } else {
77
- setError("Failed to import the JLCPCB component. Please try again.")
153
+ const errorMessage = extractErrorMessage(apiError)
154
+ setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }))
155
+
78
156
  toast({
79
157
  title: "Import Failed",
80
- description:
81
- "Failed to import the JLCPCB component. Please try again.",
158
+ description: errorMessage,
82
159
  variant: "destructive",
83
160
  })
84
161
  }
85
- } finally {
86
- setIsLoading(false)
162
+
163
+ return { success: false }
164
+ }
165
+ }
166
+
167
+ return {
168
+ ...state,
169
+ importComponent,
170
+ resetState,
171
+ }
172
+ }
173
+
174
+ export function JLCPCBImportDialog({
175
+ open,
176
+ onOpenChange,
177
+ }: JLCPCBImportDialogProps) {
178
+ const [partNumber, setPartNumber] = useState("")
179
+ const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
180
+
181
+ const { isLoading, error, existingComponent, importComponent, resetState } =
182
+ useJLCPCBImport()
183
+
184
+ const handleImport = async () => {
185
+ const result = await importComponent(partNumber)
186
+ if (result.success) {
187
+ onOpenChange(false)
87
188
  }
88
189
  }
89
190
 
191
+ const handleInputChange = (value: string) => {
192
+ setPartNumber(value)
193
+ resetState()
194
+ }
195
+
196
+ const createGitHubIssue = () => {
197
+ const issueTitle = `[${partNumber}] Failed to import from JLCPCB`
198
+ const issueBody = `I tried to import the part number ${partNumber} from JLCPCB, but it failed. Here's the error I got:\n\`\`\`\n${error}\n\`\`\`\n\nCould be an issue in \`fetchEasyEDAComponent\` or \`convertRawEasyEdaToTs\``
199
+ const issueLabels = "snippets,good first issue"
200
+ const url = `https://github.com/tscircuit/easyeda-converter/issues/new?title=${encodeURIComponent(
201
+ issueTitle,
202
+ )}&body=${encodeURIComponent(issueBody)}&labels=${encodeURIComponent(issueLabels)}`
203
+ window.open(url, "_blank")
204
+ }
205
+
90
206
  return (
91
207
  <Dialog open={open} onOpenChange={onOpenChange}>
92
208
  <DialogContent>
@@ -96,6 +212,7 @@ export function JLCPCBImportDialog({
96
212
  Enter the JLCPCB part number to import the component.
97
213
  </DialogDescription>
98
214
  </DialogHeader>
215
+
99
216
  <div className="py-4 text-center">
100
217
  <a
101
218
  href="https://yaqwsx.github.io/jlcparts/#/"
@@ -105,16 +222,13 @@ export function JLCPCBImportDialog({
105
222
  >
106
223
  JLCPCB Part Search
107
224
  </a>
225
+
108
226
  <Input
109
227
  className="mt-3"
110
228
  placeholder="Enter JLCPCB part number (e.g., C46749)"
111
229
  value={partNumber}
112
230
  disabled={isLoading}
113
- onChange={(e) => {
114
- setPartNumber(e.target.value)
115
- setError(null)
116
- setHasBeenImportedToAccountAlready(false)
117
- }}
231
+ onChange={(e) => handleInputChange(e.target.value)}
118
232
  onKeyDown={(e) => {
119
233
  if (
120
234
  e.key === "Enter" &&
@@ -126,43 +240,31 @@ export function JLCPCBImportDialog({
126
240
  }
127
241
  }}
128
242
  />
129
- {error && !hasBeenImportedToAccountAlready && (
130
- <p className="bg-red-100 p-2 mt-2 pre-wrap">{error}</p>
131
- )}
132
243
 
133
- {error && !hasBeenImportedToAccountAlready && (
134
- <div className="flex justify-end mt-2">
135
- <Button
136
- variant="default"
137
- onClick={() => {
138
- const issueTitle = `[${partNumber}] Failed to import from JLCPCB`
139
- const issueBody = `I tried to import the part number ${partNumber} from JLCPCB, but it failed. Here's the error I got:\n\`\`\`\n${error}\n\`\`\`\n\nCould be an issue in \`fetchEasyEDAComponent\` or \`convertRawEasyEdaToTs\``
140
- const issueLabels = "snippets,good first issue"
141
- const url = `https://github.com/tscircuit/easyeda-converter/issues/new?title=${encodeURIComponent(
142
- issueTitle,
143
- )}&body=${encodeURIComponent(
144
- issueBody,
145
- )}&labels=${encodeURIComponent(issueLabels)}`
146
- window.open(url, "_blank")
147
- }}
148
- >
149
- File Issue on GitHub (prefilled)
150
- </Button>
151
- </div>
244
+ {error && !existingComponent && (
245
+ <>
246
+ <p className="bg-red-100 p-2 mt-2 pre-wrap">{error}</p>
247
+ <div className="flex justify-end mt-2">
248
+ <Button variant="default" onClick={createGitHubIssue}>
249
+ File Issue on GitHub (prefilled)
250
+ </Button>
251
+ </div>
252
+ </>
152
253
  )}
153
254
 
154
- {hasBeenImportedToAccountAlready && (
255
+ {existingComponent && (
155
256
  <p className="p-2 mt-2 pre-wrap text-md text-green-600">
156
257
  This part number has already been imported to your profile.{" "}
157
258
  <PrefetchPageLink
158
259
  className="text-blue-500 hover:underline"
159
- href={`/${session?.github_username}/${partNumber}`}
260
+ href={`/${existingComponent.username}/${existingComponent.partNumber}`}
160
261
  >
161
262
  View it here
162
263
  </PrefetchPageLink>
163
264
  </p>
164
265
  )}
165
266
  </div>
267
+
166
268
  <DialogFooter>
167
269
  <Button onClick={handleImport} disabled={isLoading || !isLoggedIn}>
168
270
  {!isLoggedIn
@@ -23,26 +23,33 @@ export const LogContent = ({
23
23
  error?: ErrorObject | string | null
24
24
  }) => {
25
25
  return (
26
- <div className="whitespace-pre-wrap font-mono text-xs">
26
+ <div className="font-mono text-xs space-y-1 min-w-0">
27
27
  {logs.map(
28
28
  (log, i) =>
29
29
  log.timestamp &&
30
30
  log.message && (
31
31
  <div
32
32
  key={i}
33
- className={
33
+ className={`break-words whitespace-pre-wrap ${
34
34
  log.type === "error"
35
35
  ? "text-red-600"
36
36
  : log.type === "success"
37
37
  ? "text-green-600"
38
38
  : "text-gray-600"
39
- }
39
+ }`}
40
40
  >
41
- {new Date(log.timestamp).toLocaleTimeString()} {log.message}
41
+ <span className="text-gray-500 whitespace-nowrap">
42
+ {new Date(log.timestamp).toLocaleTimeString()}
43
+ </span>{" "}
44
+ <span className="break-all">{log.message}</span>
42
45
  </div>
43
46
  ),
44
47
  )}
45
- {error && <div className="text-red-600">{getErrorText(error)}</div>}
48
+ {error && (
49
+ <div className="text-red-600 break-words whitespace-pre-wrap">
50
+ {getErrorText(error)}
51
+ </div>
52
+ )}
46
53
  </div>
47
54
  )
48
55
  }
@@ -1,13 +1,13 @@
1
1
  "use client"
2
2
 
3
+ import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
4
+ import { PackageRelease } from "fake-snippets-api/lib/db/schema"
3
5
  import { useState } from "react"
6
+ import { LogContent } from "./LogContent"
4
7
  import { BuildPreviewContent } from "./build-preview-content"
8
+ import { CollapsibleSection } from "./collapsible-section"
5
9
  import { PackageBuildDetailsPanel } from "./package-build-details-panel"
6
10
  import { PackageBuildHeader } from "./package-build-header"
7
- import { CollapsibleSection } from "./collapsible-section"
8
- import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
9
- import { LogContent } from "./LogContent"
10
- import { PackageRelease } from "fake-snippets-api/lib/db/schema"
11
11
 
12
12
  function computeDuration(
13
13
  startedAt: string | null | undefined,
@@ -18,7 +18,10 @@ function computeDuration(
18
18
  }
19
19
 
20
20
  export const PackageBuildDetailsPage = () => {
21
- const { packageRelease } = useCurrentPackageRelease({ include_logs: true })
21
+ const { packageRelease } = useCurrentPackageRelease({
22
+ include_logs: true,
23
+ refetchInterval: 2000,
24
+ })
22
25
  const [openSections, setOpenSections] = useState<Record<string, boolean>>({})
23
26
 
24
27
  const {
@@ -73,7 +76,6 @@ export const PackageBuildDetailsPage = () => {
73
76
  transpilation_completed_at,
74
77
  )}
75
78
  displayStatus={transpilation_display_status}
76
- error={transpilation_error}
77
79
  isOpen={openSections.summary}
78
80
  onToggle={() => toggleSection("summary")}
79
81
  >
@@ -94,7 +96,6 @@ export const PackageBuildDetailsPage = () => {
94
96
  circuit_json_build_completed_at,
95
97
  )}
96
98
  displayStatus={circuit_json_build_display_status}
97
- error={circuit_json_build_error}
98
99
  isOpen={openSections.logs}
99
100
  onToggle={() => toggleSection("logs")}
100
101
  >
@@ -2,7 +2,7 @@ import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
2
2
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
3
 
4
4
  export function BuildPreviewContent() {
5
- const { packageRelease } = useCurrentPackageRelease()
5
+ const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
6
6
  const { packageInfo } = useCurrentPackageInfo()
7
7
 
8
8
  if (!packageRelease) {
@@ -1,6 +1,5 @@
1
1
  import type React from "react"
2
- import { ChevronRight, CheckCircle2 } from "lucide-react"
3
- import { Badge } from "@/components/ui/badge"
2
+ import { ChevronRight } from "lucide-react"
4
3
  import {
5
4
  Collapsible,
6
5
  CollapsibleContent,
@@ -8,16 +7,9 @@ import {
8
7
  } from "@/components/ui/collapsible"
9
8
  import { getColorForDisplayStatus } from "./getColorForDisplayStatus"
10
9
  import { PackageRelease } from "fake-snippets-api/lib/db/schema"
11
- import { ErrorObjectOrString, getErrorText } from "./ErrorObject"
10
+ import { ErrorObjectOrString } from "./ErrorObject"
12
11
  import { capitalCase } from "./capitalCase"
13
12
 
14
- type BadgeInfo = {
15
- text: string
16
- variant?: "default" | "secondary" | "destructive"
17
- className?: string
18
- icon?: React.ReactNode
19
- }
20
-
21
13
  interface CollapsibleSectionProps {
22
14
  title: string
23
15
  duration?: string
@@ -25,69 +17,45 @@ interface CollapsibleSectionProps {
25
17
  displayStatus?: PackageRelease["display_status"]
26
18
  isOpen: boolean
27
19
  onToggle: () => void
28
- badges?: Array<BadgeInfo>
29
20
  children?: React.ReactNode
30
21
  }
31
22
 
32
23
  export function CollapsibleSection({
33
24
  title,
34
25
  duration,
35
- error,
36
26
  displayStatus,
37
27
  isOpen,
38
28
  onToggle,
39
- badges = [],
40
29
  children,
41
30
  }: CollapsibleSectionProps) {
42
31
  return (
43
32
  <Collapsible open={isOpen} onOpenChange={onToggle}>
44
33
  <CollapsibleTrigger asChild>
45
- <div className="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100">
46
- <div className="flex items-center gap-2">
34
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-4 bg-white border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100">
35
+ <div className="flex items-center gap-2 min-w-0">
47
36
  <ChevronRight
48
- className={`w-4 h-4 transition-transform ${isOpen ? "rotate-90" : ""}`}
37
+ className={`w-4 h-4 flex-shrink-0 transition-transform ${isOpen ? "rotate-90" : ""}`}
49
38
  />
50
- <span className="font-medium">{title}</span>
39
+ <span className="font-medium truncate">{title}</span>
51
40
  </div>
52
- <div className="flex items-center gap-2">
53
- {[
54
- ...badges,
55
- ...(error
56
- ? [
57
- {
58
- text: getErrorText(error),
59
- variant: "destructive",
60
- } as BadgeInfo,
61
- ]
62
- : []),
63
- ].map((badge, index) => (
64
- <Badge
65
- key={index}
66
- variant={badge.variant || "secondary"}
67
- className={
68
- badge.className ||
69
- "bg-gray-200 text-gray-700 flex items-center gap-1"
70
- }
71
- >
72
- {badge.icon}
73
- {badge.text}
74
- </Badge>
75
- ))}
41
+ <div className="flex items-center gap-2 flex-shrink-0 ml-6 sm:ml-0">
76
42
  {duration && (
77
- <span className="text-sm text-gray-600">{duration}</span>
43
+ <span className="text-sm text-gray-600 whitespace-nowrap">
44
+ {duration}
45
+ </span>
78
46
  )}
79
47
  <div
80
- className={`w-2 h-2 rounded-lg ${getColorForDisplayStatus(displayStatus)}`}
48
+ className={`w-2 h-2 rounded-lg flex-shrink-0 ${getColorForDisplayStatus(displayStatus)}`}
81
49
  />
82
- <div className="text-gray-600 text-xs font-medium">
50
+ <div className="text-gray-600 text-xs font-medium whitespace-nowrap">
83
51
  {capitalCase(displayStatus) || "???"}
84
52
  </div>
85
53
  </div>
86
54
  </div>
87
55
  </CollapsibleTrigger>
88
56
  <CollapsibleContent>
89
- <div className="p-4 bg-white border-x border-b border-gray-200 rounded-b-lg">
90
- {children}
57
+ <div className="bg-white border-x border-b border-gray-200 rounded-b-lg overflow-hidden">
58
+ <div className="p-4 overflow-x-auto max-w-full">{children}</div>
91
59
  </div>
92
60
  </CollapsibleContent>
93
61
  </Collapsible>
@@ -1,8 +1,9 @@
1
- import { Globe, GitBranch, GitCommit, Clock } from "lucide-react"
2
1
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
- import { useParams } from "wouter"
2
+ import { useNow } from "@/hooks/use-now"
4
3
  import { timeAgo } from "@/lib/utils/timeAgo"
5
4
  import { PackageRelease } from "fake-snippets-api/lib/db/schema"
5
+ import { Clock, GitBranch, GitCommit, Globe } from "lucide-react"
6
+ import { useParams } from "wouter"
6
7
 
7
8
  const capitalCase = (str: string) => {
8
9
  return str.charAt(0).toUpperCase() + str.slice(1)
@@ -24,8 +25,9 @@ function getColorFromDisplayStatus(
24
25
  }
25
26
 
26
27
  export function PackageBuildDetailsPanel() {
27
- const { packageRelease } = useCurrentPackageRelease()
28
+ const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
28
29
  const { author } = useParams() // TODO use packageRelease.author_account_id when it's added by backed
30
+ const now = useNow(1000)
29
31
 
30
32
  if (!packageRelease) {
31
33
  // TODO show skeleton instead
@@ -55,6 +57,24 @@ export function PackageBuildDetailsPanel() {
55
57
  commit_sha,
56
58
  } = packageRelease
57
59
 
60
+ const buildStartedAt = (() => {
61
+ if (transpilation_started_at && circuit_json_build_started_at) {
62
+ return new Date(transpilation_started_at) <
63
+ new Date(circuit_json_build_started_at)
64
+ ? transpilation_started_at
65
+ : circuit_json_build_started_at
66
+ }
67
+ return transpilation_started_at || circuit_json_build_started_at || null
68
+ })()
69
+
70
+ const buildCompletedAt =
71
+ circuit_json_build_completed_at || transpilation_completed_at || null
72
+
73
+ const elapsedMs = buildStartedAt
74
+ ? (buildCompletedAt ? new Date(buildCompletedAt).getTime() : now) -
75
+ new Date(buildStartedAt).getTime()
76
+ : null
77
+
58
78
  return (
59
79
  <div className="space-y-6 bg-white p-4 border border-gray-200 rounded-lg">
60
80
  {/* Created */}
@@ -92,15 +112,13 @@ export function PackageBuildDetailsPanel() {
92
112
  <h3 className="text-sm font-medium text-gray-600 mb-2">Build Time</h3>
93
113
  <div className="flex items-center gap-2">
94
114
  <Clock className="w-4 h-4 text-gray-500" />
95
- {circuit_json_build_completed_at && (
96
- <span className="text-sm">
97
- {total_build_duration_ms
98
- ? `${Math.floor(total_build_duration_ms / 1000)}s`
99
- : ""}
100
- </span>
115
+ {elapsedMs !== null && (
116
+ <span className="text-sm">{Math.floor(elapsedMs / 1000)}s</span>
101
117
  )}
102
118
  <span className="text-sm text-gray-500">
103
- {timeAgo(circuit_json_build_completed_at, "waiting...")}
119
+ {buildStartedAt
120
+ ? `Started ${timeAgo(buildStartedAt)}`
121
+ : "waiting..."}
104
122
  </span>
105
123
  </div>
106
124
  </div>
@@ -1,13 +1,15 @@
1
- import { Github, RefreshCw } from "lucide-react"
2
1
  import { Button } from "@/components/ui/button"
3
- import { useParams } from "wouter"
4
- import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
5
2
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
6
3
  import { useRebuildPackageReleaseMutation } from "@/hooks/use-rebuild-package-release-mutation"
4
+ import { Github, RefreshCw, RotateCcw } from "lucide-react"
5
+ import { useParams } from "wouter"
6
+ import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
7
7
 
8
8
  export function PackageBuildHeader() {
9
9
  const { author, packageName } = useParams()
10
- const { packageRelease } = useCurrentPackageRelease()
10
+ const { packageRelease, refetch, isFetching } = useCurrentPackageRelease({
11
+ include_logs: true,
12
+ })
11
13
  const { mutate: rebuildPackage, isLoading } =
12
14
  useRebuildPackageReleaseMutation()
13
15
 
@@ -54,6 +56,16 @@ export function PackageBuildHeader() {
54
56
  <RefreshCw className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2" />
55
57
  {isLoading ? "Rebuilding..." : "Rebuild"}
56
58
  </Button>
59
+ <Button
60
+ variant="outline"
61
+ size="icon"
62
+ aria-label="Reload logs"
63
+ className="border-gray-300 bg-white hover:bg-gray-50"
64
+ onClick={() => refetch()}
65
+ disabled={isFetching}
66
+ >
67
+ <RotateCcw className="w-3 h-3 sm:w-4 sm:h-4" />
68
+ </Button>
57
69
  <DownloadButtonAndMenu
58
70
  snippetUnscopedName={`${author}/${packageName}`}
59
71
  />