mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-03-28 17:42:55 +01:00
lint fixes
This commit is contained in:
26
.github/workflows/custom_build.yml
vendored
26
.github/workflows/custom_build.yml
vendored
@@ -4,30 +4,30 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Target board (e.g. rak4631)'
|
||||
description: "Target board (e.g. rak4631)"
|
||||
required: true
|
||||
type: string
|
||||
flags:
|
||||
description: 'Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)'
|
||||
description: "Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)"
|
||||
required: false
|
||||
type: string
|
||||
version:
|
||||
description: 'Firmware Version (Tag/Branch)'
|
||||
description: "Firmware Version (Tag/Branch)"
|
||||
required: true
|
||||
build_id:
|
||||
description: 'Convex Build ID'
|
||||
description: "Convex Build ID"
|
||||
required: true
|
||||
type: string
|
||||
build_hash:
|
||||
description: 'Build hash for artifact naming'
|
||||
description: "Build hash for artifact naming"
|
||||
required: true
|
||||
type: string
|
||||
convex_url:
|
||||
description: 'Convex Site URL'
|
||||
description: "Convex Site URL"
|
||||
required: true
|
||||
type: string
|
||||
plugins:
|
||||
description: 'Space-separated plugin slugs to install'
|
||||
description: "Space-separated plugin slugs to install"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Update Status - Fetching Firmware
|
||||
shell: bash
|
||||
@@ -160,13 +160,13 @@ jobs:
|
||||
- PlatformIO dependencies (\`.pio/\`) are not included - PlatformIO will download these as needed
|
||||
- The build flags above must be set exactly as shown to reproduce the build
|
||||
EOF
|
||||
|
||||
|
||||
# Create MESHFORGE.md so it gets included in the archive
|
||||
# (already created above, just ensuring it exists)
|
||||
|
||||
|
||||
# Define archive suffix for consistent naming
|
||||
ARTIFACT_ARCHIVE_SUFFIX="-${{ inputs.build_hash }}-${{ github.run_id }}.tar.gz"
|
||||
|
||||
|
||||
# Create archive from working directory to include plugins installed by mpm
|
||||
# Exclude .git, .pio, and build artifacts
|
||||
cd ..
|
||||
@@ -177,12 +177,12 @@ jobs:
|
||||
--exclude='build' \
|
||||
-czf "source${ARTIFACT_ARCHIVE_SUFFIX}" \
|
||||
-C firmware .
|
||||
|
||||
|
||||
update_status uploading_source_archive
|
||||
|
||||
SOURCE_ARCHIVE_PATH="/source${ARTIFACT_ARCHIVE_SUFFIX}"
|
||||
SOURCE_OBJECT_PATH="${R2_BUCKET_NAME}/source${ARTIFACT_ARCHIVE_SUFFIX}"
|
||||
|
||||
|
||||
# Upload source archive to R2
|
||||
wrangler r2 object put "$SOURCE_OBJECT_PATH" \
|
||||
--file "source${ARTIFACT_ARCHIVE_SUFFIX}" --remote
|
||||
|
||||
26
.github/workflows/custom_build_test.yml
vendored
26
.github/workflows/custom_build_test.yml
vendored
@@ -4,30 +4,30 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Target board (e.g. rak4631)'
|
||||
description: "Target board (e.g. rak4631)"
|
||||
required: true
|
||||
type: string
|
||||
flags:
|
||||
description: 'Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)'
|
||||
description: "Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)"
|
||||
required: false
|
||||
type: string
|
||||
version:
|
||||
description: 'Firmware Version (Tag/Branch)'
|
||||
description: "Firmware Version (Tag/Branch)"
|
||||
required: true
|
||||
build_id:
|
||||
description: 'Convex Build ID'
|
||||
description: "Convex Build ID"
|
||||
required: true
|
||||
type: string
|
||||
build_hash:
|
||||
description: 'Build hash for artifact naming'
|
||||
description: "Build hash for artifact naming"
|
||||
required: true
|
||||
type: string
|
||||
convex_url:
|
||||
description: 'Convex Site URL'
|
||||
description: "Convex Site URL"
|
||||
required: true
|
||||
type: string
|
||||
plugins:
|
||||
description: 'Space-separated plugin slugs to install'
|
||||
description: "Space-separated plugin slugs to install"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Update Status - Fetching Firmware
|
||||
shell: bash
|
||||
@@ -160,13 +160,13 @@ jobs:
|
||||
- PlatformIO dependencies (\`.pio/\`) are not included - PlatformIO will download these as needed
|
||||
- The build flags above must be set exactly as shown to reproduce the build
|
||||
EOF
|
||||
|
||||
|
||||
# Create MESHFORGE.md so it gets included in the archive
|
||||
# (already created above, just ensuring it exists)
|
||||
|
||||
|
||||
# Define archive suffix for consistent naming
|
||||
ARTIFACT_ARCHIVE_SUFFIX="-${{ inputs.build_hash }}-${{ github.run_id }}.tar.gz"
|
||||
|
||||
|
||||
# Create archive from working directory to include plugins installed by mpm
|
||||
# Exclude .git, .pio, and build artifacts
|
||||
cd ..
|
||||
@@ -177,12 +177,12 @@ jobs:
|
||||
--exclude='build' \
|
||||
-czf "source${ARTIFACT_ARCHIVE_SUFFIX}" \
|
||||
-C firmware .
|
||||
|
||||
|
||||
update_status uploading_source_archive
|
||||
|
||||
SOURCE_ARCHIVE_PATH="/source${ARTIFACT_ARCHIVE_SUFFIX}"
|
||||
SOURCE_OBJECT_PATH="${R2_BUCKET_NAME}/source${ARTIFACT_ARCHIVE_SUFFIX}"
|
||||
|
||||
|
||||
# Upload source archive to R2
|
||||
wrangler r2 object put "$SOURCE_OBJECT_PATH" \
|
||||
--file "source${ARTIFACT_ARCHIVE_SUFFIX}" --remote
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
vendor
|
||||
@@ -18,4 +18,4 @@
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,27 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DiscordIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.1.1 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.1 16.1 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface DiscordButtonProps {
|
||||
variant?: 'default' | 'outline' | 'ghost' | 'link' | 'destructive'
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||
variant?: "default" | "outline" | "ghost" | "link" | "destructive"
|
||||
size?: "default" | "sm" | "lg" | "icon"
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DiscordButton({
|
||||
variant = 'outline',
|
||||
size,
|
||||
className,
|
||||
}: DiscordButtonProps) {
|
||||
export function DiscordButton({ variant = "outline", size, className }: DiscordButtonProps) {
|
||||
return (
|
||||
<a
|
||||
href="https://discord.gg/8KgJpvjfaJ"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn('flex items-center gap-2', className)}
|
||||
>
|
||||
<a href="https://discord.gg/8KgJpvjfaJ" target="_blank" rel="noopener noreferrer">
|
||||
<Button variant={variant} size={size} className={cn("flex items-center gap-2", className)}>
|
||||
<DiscordIcon className="w-4 h-4" />
|
||||
Discord
|
||||
</Button>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { usePageContext } from "vike-react/usePageContext";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ReactNode } from "react"
|
||||
import { usePageContext } from "vike-react/usePageContext"
|
||||
|
||||
export function Link({ href, children }: { href: string; children: ReactNode }) {
|
||||
const pageContext = usePageContext();
|
||||
const { urlPathname } = pageContext;
|
||||
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href);
|
||||
const pageContext = usePageContext()
|
||||
const { urlPathname } = pageContext
|
||||
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href)
|
||||
return (
|
||||
<a href={href} className={isActive ? "is-active" : undefined}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,23 +5,14 @@ interface ModuleCardProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function ModuleCard({
|
||||
name,
|
||||
description,
|
||||
selected,
|
||||
onClick,
|
||||
}: ModuleCardProps) {
|
||||
export function ModuleCard({ name, description, selected, onClick }: ModuleCardProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`
|
||||
w-full text-left p-4 rounded-lg border-2 transition-all
|
||||
${
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-500/10'
|
||||
: 'border-slate-700 bg-slate-900/50 hover:border-slate-600'
|
||||
}
|
||||
${selected ? "border-blue-500 bg-blue-500/10" : "border-slate-700 bg-slate-900/50 hover:border-slate-600"}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -29,32 +20,20 @@ export function ModuleCard({
|
||||
<div
|
||||
className={`
|
||||
w-5 h-5 rounded border-2 flex items-center justify-center
|
||||
${selected ? 'border-blue-500 bg-blue-500' : 'border-slate-500'}
|
||||
${selected ? "border-blue-500 bg-blue-500" : "border-slate-500"}
|
||||
`}
|
||||
>
|
||||
{selected && (
|
||||
<svg
|
||||
className="w-3 h-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<title>Checkmark</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-sm mb-1">{name}</h4>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
|
||||
interface ModuleToggleProps {
|
||||
id: string
|
||||
@@ -8,12 +8,7 @@ interface ModuleToggleProps {
|
||||
onToggle: (excluded: boolean) => void
|
||||
}
|
||||
|
||||
export function ModuleToggle({
|
||||
name,
|
||||
description,
|
||||
isExcluded,
|
||||
onToggle,
|
||||
}: ModuleToggleProps) {
|
||||
export function ModuleToggle({ name, description, isExcluded, onToggle }: ModuleToggleProps) {
|
||||
return (
|
||||
<div className="flex items-start gap-4 p-4 rounded-lg border-2 border-slate-700 bg-slate-900/50 hover:border-slate-600 transition-colors">
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -26,7 +21,7 @@ export function ModuleToggle({
|
||||
onCheckedChange={onToggle}
|
||||
labelLeft="Default"
|
||||
labelRight="Excluded"
|
||||
className={isExcluded ? 'bg-orange-600' : 'bg-slate-600'}
|
||||
className={isExcluded ? "bg-orange-600" : "bg-slate-600"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExternalLink, Star } from 'lucide-react'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { ExternalLink, Star } from "lucide-react"
|
||||
|
||||
interface PluginToggleProps {
|
||||
id: string
|
||||
@@ -26,17 +26,17 @@ export function PluginToggle({
|
||||
homepage,
|
||||
version,
|
||||
disabled = false,
|
||||
enabledLabel = 'Add',
|
||||
enabledLabel = "Add",
|
||||
incompatibleReason,
|
||||
}: PluginToggleProps) {
|
||||
const isIncompatible = !!incompatibleReason
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative flex items-start gap-4 p-4 rounded-lg border-2 transition-colors ${
|
||||
isIncompatible
|
||||
? 'border-slate-800 bg-slate-900/30 opacity-60 cursor-not-allowed'
|
||||
: 'border-slate-700 bg-slate-900/50 hover:border-slate-600'
|
||||
? "border-slate-800 bg-slate-900/30 opacity-60 cursor-not-allowed"
|
||||
: "border-slate-700 bg-slate-900/50 hover:border-slate-600"
|
||||
}`}
|
||||
>
|
||||
{/* Flash count and homepage links in lower right */}
|
||||
@@ -63,7 +63,7 @@ export function PluginToggle({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-slate-400 hover:text-slate-300 transition-colors"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
@@ -71,20 +71,14 @@ export function PluginToggle({
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<h4 className={`font-semibold text-sm ${isIncompatible ? 'text-slate-500' : ''}`}>
|
||||
{name}
|
||||
</h4>
|
||||
{featured && (
|
||||
<Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />
|
||||
)}
|
||||
<h4 className={`font-semibold text-sm ${isIncompatible ? "text-slate-500" : ""}`}>{name}</h4>
|
||||
{featured && <Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />}
|
||||
</div>
|
||||
<p className={`text-xs leading-relaxed ${isIncompatible ? 'text-slate-500' : 'text-slate-400'}`}>
|
||||
<p className={`text-xs leading-relaxed ${isIncompatible ? "text-slate-500" : "text-slate-400"}`}>
|
||||
{description}
|
||||
</p>
|
||||
{isIncompatible && incompatibleReason && (
|
||||
<p className="text-xs text-red-400 mt-1 font-medium">
|
||||
{incompatibleReason}
|
||||
</p>
|
||||
<p className="text-xs text-red-400 mt-1 font-medium">{incompatibleReason}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 shrink-0">
|
||||
@@ -94,7 +88,7 @@ export function PluginToggle({
|
||||
disabled={disabled}
|
||||
labelLeft="Skip"
|
||||
labelRight={enabledLabel}
|
||||
className={isEnabled ? 'bg-green-600' : 'bg-slate-600'}
|
||||
className={isEnabled ? "bg-green-600" : "bg-slate-600"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<mask id="SVGfUZuVbjp">
|
||||
<g fill="#fff">
|
||||
<path
|
||||
@@ -23,48 +16,19 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
strokeWidth="2"
|
||||
d="M12 9.42c4.42 0 8 2.37 8 5.29c0 2.92 -3.58 5.29 -8 5.29c-4.42 0 -8 -2.37 -8 -5.29c0 -2.92 3.58 -5.29 8 -5.29Z"
|
||||
>
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="fill-opacity"
|
||||
begin="0.6s"
|
||||
dur="0.4s"
|
||||
values="0;1"
|
||||
/>
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="stroke-dashoffset"
|
||||
dur="0.6s"
|
||||
values="48;0"
|
||||
/>
|
||||
<animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.4s" values="0;1" />
|
||||
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="48;0" />
|
||||
</path>
|
||||
<circle cx="7.24" cy="11.97" r="2.24" opacity="0">
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="cx"
|
||||
begin="1s"
|
||||
dur="0.2s"
|
||||
values="7.24;3.94"
|
||||
/>
|
||||
<animate fill="freeze" attributeName="cx" begin="1s" dur="0.2s" values="7.24;3.94" />
|
||||
<set fill="freeze" attributeName="opacity" begin="1s" to="1" />
|
||||
</circle>
|
||||
<circle cx="16.76" cy="11.97" r="2.24" opacity="0">
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="cx"
|
||||
begin="1s"
|
||||
dur="0.2s"
|
||||
values="16.76;20.06"
|
||||
/>
|
||||
<animate fill="freeze" attributeName="cx" begin="1s" dur="0.2s" values="16.76;20.06" />
|
||||
<set fill="freeze" attributeName="opacity" begin="1s" to="1" />
|
||||
</circle>
|
||||
<circle cx="18.45" cy="4.23" r="1.61" opacity="0">
|
||||
<animate
|
||||
attributeName="cx"
|
||||
begin="2.4s"
|
||||
dur="6s"
|
||||
repeatCount="indefinite"
|
||||
values="18.45;5.75;18.45"
|
||||
/>
|
||||
<animate attributeName="cx" begin="2.4s" dur="6s" repeatCount="indefinite" values="18.45;5.75;18.45" />
|
||||
<set fill="freeze" attributeName="opacity" begin="2.6s" to="1" />
|
||||
</circle>
|
||||
</g>
|
||||
@@ -85,32 +49,14 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
repeatCount="indefinite"
|
||||
values="M12 8.75L13.18 3.11L18.21 4.18;M12 8.75L12 2L12 4.18;M12 8.75L10.82 3.11L5.79 4.18;M12 8.75L12 2L12 4.18;M12 8.75L13.18 3.11L18.21 4.18"
|
||||
/>
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="stroke-dashoffset"
|
||||
begin="2.4s"
|
||||
dur="0.2s"
|
||||
values="12;0"
|
||||
/>
|
||||
<animate fill="freeze" attributeName="stroke-dashoffset" begin="2.4s" dur="0.2s" values="12;0" />
|
||||
</path>
|
||||
<g fillOpacity="0">
|
||||
<circle cx="8.45" cy="13.59" r="1.61">
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="fill-opacity"
|
||||
begin="1.2s"
|
||||
dur="0.4s"
|
||||
values="0;1"
|
||||
/>
|
||||
<animate fill="freeze" attributeName="fill-opacity" begin="1.2s" dur="0.4s" values="0;1" />
|
||||
</circle>
|
||||
<circle cx="15.55" cy="13.59" r="1.61">
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="fill-opacity"
|
||||
begin="1.6s"
|
||||
dur="0.4s"
|
||||
values="0;1"
|
||||
/>
|
||||
<animate fill="freeze" attributeName="fill-opacity" begin="1.6s" dur="0.4s" values="0;1" />
|
||||
</circle>
|
||||
</g>
|
||||
<path
|
||||
@@ -123,13 +69,7 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
strokeWidth=".8"
|
||||
d="M8.47 17.52c0 0 0.94 1.06 3.53 1.06c2.58 0 3.53 -1.06 3.53 -1.06"
|
||||
>
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="stroke-dashoffset"
|
||||
begin="2s"
|
||||
dur="0.2s"
|
||||
values="10;0"
|
||||
/>
|
||||
<animate fill="freeze" attributeName="stroke-dashoffset" begin="2s" dur="0.2s" values="10;0" />
|
||||
</path>
|
||||
</mask>
|
||||
<rect width="24" height="24" fill="currentColor" mask="url(#SVGfUZuVbjp)" />
|
||||
@@ -138,31 +78,18 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
}
|
||||
|
||||
interface RedditButtonProps {
|
||||
variant?: 'default' | 'outline' | 'ghost' | 'link' | 'destructive'
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||
variant?: "default" | "outline" | "ghost" | "link" | "destructive"
|
||||
size?: "default" | "sm" | "lg" | "icon"
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function RedditButton({
|
||||
variant = 'outline',
|
||||
size,
|
||||
className,
|
||||
}: RedditButtonProps) {
|
||||
export function RedditButton({ variant = "outline", size, className }: RedditButtonProps) {
|
||||
return (
|
||||
<a
|
||||
href="https://www.reddit.com/r/MeshForge/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn('flex items-center gap-2', className)}
|
||||
>
|
||||
<a href="https://www.reddit.com/r/MeshForge/" target="_blank" rel="noopener noreferrer">
|
||||
<Button variant={variant} size={size} className={cn("flex items-center gap-2", className)}>
|
||||
<RedditIcon className="w-4 h-4" />
|
||||
Reddit
|
||||
</Button>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ interface SourceAvailableProps {
|
||||
* Component that only renders children when sourcePath is available.
|
||||
* Uses the sourcePath field from the build instead of polling.
|
||||
*/
|
||||
export function SourceAvailable({
|
||||
sourcePath,
|
||||
children,
|
||||
}: SourceAvailableProps) {
|
||||
export function SourceAvailable({ sourcePath, children }: SourceAvailableProps) {
|
||||
if (!sourcePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,57 +1,46 @@
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { Check } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
@@ -11,14 +11,12 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('grid place-content-center text-current')}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as React from 'react'
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -17,6 +17,6 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Toaster as Sonner } from 'sonner'
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = 'system' } = useTheme()
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
||||
description: 'group-[.toast]:text-muted-foreground',
|
||||
actionButton:
|
||||
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
||||
cancelButton:
|
||||
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SwitchProps {
|
||||
checked: boolean
|
||||
@@ -9,14 +9,7 @@ interface SwitchProps {
|
||||
labelRight?: string
|
||||
}
|
||||
|
||||
export function Switch({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled = false,
|
||||
className,
|
||||
labelLeft,
|
||||
labelRight,
|
||||
}: SwitchProps) {
|
||||
export function Switch({ checked, onCheckedChange, disabled = false, className, labelLeft, labelRight }: SwitchProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -25,27 +18,19 @@ export function Switch({
|
||||
disabled={disabled}
|
||||
onClick={() => !disabled && onCheckedChange(!checked)}
|
||||
className={cn(
|
||||
'relative inline-flex h-8 w-24 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
!className && (checked ? 'bg-red-600' : 'bg-slate-600'),
|
||||
"relative inline-flex h-8 w-24 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
!className && (checked ? "bg-red-600" : "bg-slate-600"),
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block h-6 w-6 transform rounded-full bg-white transition-transform',
|
||||
checked ? 'translate-x-[68px]' : 'translate-x-1'
|
||||
"inline-block h-6 w-6 transform rounded-full bg-white transition-transform",
|
||||
checked ? "translate-x-[68px]" : "translate-x-1"
|
||||
)}
|
||||
/>
|
||||
{checked && labelRight && (
|
||||
<span className="absolute left-2 text-xs font-medium text-white">
|
||||
{labelRight}
|
||||
</span>
|
||||
)}
|
||||
{!checked && labelLeft && (
|
||||
<span className="absolute right-2 text-xs font-medium text-white">
|
||||
{labelLeft}
|
||||
</span>
|
||||
)}
|
||||
{checked && labelRight && <span className="absolute left-2 text-xs font-medium text-white">{labelRight}</span>}
|
||||
{!checked && labelLeft && <span className="absolute right-2 text-xs font-medium text-white">{labelLeft}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -217,4 +217,4 @@
|
||||
"rp2350": null,
|
||||
"stm32": null,
|
||||
"portduino": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,243 +1,243 @@
|
||||
// This file is auto-generated by scripts/generate-versions.js
|
||||
export const VERSIONS = [
|
||||
'v2.7.16.a597230',
|
||||
'v2.7.15.567b8ea',
|
||||
'v2.7.14.e959000',
|
||||
'v2.7.13.597fa0b',
|
||||
'v2.7.12.45f15b8',
|
||||
'v2.7.11.ee68575',
|
||||
'v2.7.10.94d4bdf',
|
||||
'v2.7.9.70724be',
|
||||
'v2.7.8.a0c0388',
|
||||
'v2.7.7.5ae4ff9',
|
||||
'v2.7.6.834c3c5',
|
||||
'v2.7.5.ddd1499',
|
||||
'v2.7.4.c1f4f79',
|
||||
'v2.7.3.cf574c7',
|
||||
'v2.7.2.f6d3782',
|
||||
'v2.7.1.f35ca81',
|
||||
'v2.7.0.705515a',
|
||||
'v2.7.0.195b7cc',
|
||||
'v2.6.13.0561f2c',
|
||||
'v2.6.12.9861e82',
|
||||
'v2.6.11.60ec05e',
|
||||
'v2.6.10.9ce4455',
|
||||
'v2.6.9.f223b8a',
|
||||
'v2.6.8.ef9d0d7',
|
||||
'v2.6.7.2d6181f',
|
||||
'v2.6.6.54c1423',
|
||||
'v2.6.5.fc3d9f2',
|
||||
'v2.6.4.b89355f',
|
||||
'v2.6.3.d28af68',
|
||||
'v2.6.3.640e731',
|
||||
'v2.6.2.31c0e8f',
|
||||
'v2.6.1.7c3edde',
|
||||
'v2.6.0.f7afa9a',
|
||||
'v2.5.23.bf958ed',
|
||||
'v2.5.22.d1fa27d',
|
||||
'v2.5.21.447533a',
|
||||
'v2.5.20.4c97351',
|
||||
'v2.5.19.f9876cf',
|
||||
'v2.5.19.d5cd6f8',
|
||||
'v2.5.18.89ebafc',
|
||||
'v2.5.17.b4b2fd6',
|
||||
'v2.5.16.f81d3b0',
|
||||
'v2.5.15.79da236',
|
||||
'v2.5.14.f2ee0df',
|
||||
'v2.5.13.295278b',
|
||||
'v2.5.13.1a06f88',
|
||||
'v2.5.12.aa184e6',
|
||||
'v2.5.11.8e2a3e5',
|
||||
'v2.5.10.0fc5c9b',
|
||||
'v2.5.9.936260f',
|
||||
'v2.5.8.6485f03',
|
||||
'v2.5.7.f77c87d',
|
||||
'v2.5.6.d55c08d',
|
||||
'v2.5.5.e182ae7',
|
||||
'v2.5.4.8d288d5',
|
||||
'v2.5.3.a70d5ee',
|
||||
'v2.5.2.771cb52',
|
||||
'v2.5.1.c13b44b',
|
||||
'v2.5.0.e470619',
|
||||
'v2.5.0.d6dac17',
|
||||
'v2.5.0.ab7de7f',
|
||||
'v2.5.0.33eb073',
|
||||
'v2.5.0.9e55e6b',
|
||||
'v2.5.0.9ac0e26',
|
||||
'v2.4.3.efc27f2',
|
||||
'v2.4.3.91d6612',
|
||||
'v2.4.2.5b45303',
|
||||
'v2.4.1.394e0e1',
|
||||
'v2.4.0.46d7b82',
|
||||
'v2.3.15.deb7c27',
|
||||
'v2.3.14.64531fa',
|
||||
'v2.3.13.83f5ba0',
|
||||
'v2.3.12.24458a7',
|
||||
'v2.3.11.2740a56',
|
||||
'v2.3.10.d19607b',
|
||||
'v2.3.9.f06c56a',
|
||||
'v2.3.8.d490a33',
|
||||
'v2.3.7.30fbcab',
|
||||
'v2.3.6.7a3570a',
|
||||
'v2.3.5.2f9b68e',
|
||||
'v2.3.4.ea61808',
|
||||
'v2.3.3.8187fa7',
|
||||
'v2.3.2.63df972',
|
||||
'v2.3.1.4fa7f5a',
|
||||
'v2.3.0.5f47ca1',
|
||||
'v2.2.24.e6a2c06',
|
||||
'v2.2.23.5672e68',
|
||||
'v2.2.22.404d0dd',
|
||||
'v2.2.21.7f7c5cb',
|
||||
'v2.2.20.af5ac32',
|
||||
'v2.2.19.8f6a283',
|
||||
'v2.2.18.e9bde80',
|
||||
'v2.2.17.dbac2b1',
|
||||
'v2.2.16.1c6acfd',
|
||||
'v2.2.15.31c4693',
|
||||
'v2.2.14.57542ce',
|
||||
'v2.2.13.f570204',
|
||||
'v2.2.12.092e6f2',
|
||||
'v2.2.11.10265aa',
|
||||
'v2.2.10.7cebd79',
|
||||
'v2.2.9.47301a5',
|
||||
'v2.2.8.61f6fb2',
|
||||
'v2.2.7.e8970ad',
|
||||
'v2.2.6.b53cb38',
|
||||
'v2.2.5.8255128',
|
||||
'v2.2.4.3bcab0e',
|
||||
'v2.2.3.282cc0b',
|
||||
'v2.2.2.f35c7be',
|
||||
'v2.2.1.fb5f2e4',
|
||||
'v2.2.0.9f6584b',
|
||||
'v2.1.23.04bbdc6',
|
||||
'v2.1.22.191a69d',
|
||||
'v2.1.21.97d7a89',
|
||||
'v2.1.20.470363d',
|
||||
'v2.1.19.eb7025f',
|
||||
'v2.1.18.de53280',
|
||||
'v2.1.17.7ca2e81',
|
||||
'v2.1.16.a2c5b92',
|
||||
'v2.1.15.cd78723',
|
||||
'v2.1.14.99a31c1',
|
||||
'v2.1.13.7475c86',
|
||||
'v2.1.12.7711b03',
|
||||
'v2.1.11.5ec624d',
|
||||
'v2.1.10.7ef12c7',
|
||||
'v2.1.9.d43ddc9',
|
||||
'v2.1.8.ee971e3',
|
||||
'v2.1.7.242f880',
|
||||
'v2.1.6.5679a82',
|
||||
'v2.1.5.23272da',
|
||||
'v2.1.4.958d2cf',
|
||||
'v2.1.3.8c68d88',
|
||||
'v2.1.2.6d20215',
|
||||
'v2.1.1.dc2ca9c',
|
||||
'v2.1.0.331a1af',
|
||||
'v2.0.23.7bb281d',
|
||||
'v2.0.22.fbfd0f1',
|
||||
'v2.0.21.83e6cea',
|
||||
'v2.0.20.7100416',
|
||||
'v2.0.19.3209aea',
|
||||
'v2.0.18.1a7991c',
|
||||
'v2.0.17.5d1c06b',
|
||||
'v2.0.16.2242b68',
|
||||
'v2.0.15.aafbde0',
|
||||
'v2.0.14.2baaad8',
|
||||
'v2.0.13.7e27729',
|
||||
'v2.0.12.2400dd4',
|
||||
'v2.0.11.8914d1a',
|
||||
'v2.0.10.e09b12c',
|
||||
'v2.0.9.6ea0963',
|
||||
'v2.0.8.090e166',
|
||||
'v2.0.7.91ff7b9',
|
||||
'v2.0.6.97fd5cf',
|
||||
'v2.0.5.65e8209',
|
||||
'v2.0.4.5417671',
|
||||
'v2.0.3.09fe616',
|
||||
'v2.0.2.8146e84',
|
||||
'v2.0.1.ad05b91',
|
||||
'v2.0.0.18ab874',
|
||||
'v1.3.48.82bcd39',
|
||||
'v1.3.47.05147c0',
|
||||
'v1.3.46.d4ea956',
|
||||
'v1.3.45.b0d0552',
|
||||
'v1.3.44.4fa8d02',
|
||||
'v1.3.43.aae9d2f',
|
||||
'v1.3.42.9bd9252',
|
||||
'v1.3.41.80ddb81',
|
||||
'v1.3.40.e87ecc2',
|
||||
'v1.3.39.ddc3727',
|
||||
'v1.3.38.1253abd',
|
||||
'v1.3.37.97712a9',
|
||||
'v1.3.36.dd720f2',
|
||||
'v1.3.36.64f852e',
|
||||
'v1.3.36.7e03019',
|
||||
'v1.3.35.3251cd5',
|
||||
'v1.3.34.401b5d9',
|
||||
'v1.3.33.ab0095c',
|
||||
'v1.3.32.7e6c22f',
|
||||
'v1.3.31.0084643',
|
||||
'v1.3.30.9fe2ddb',
|
||||
'v1.3.29.7afc149',
|
||||
'v1.3.28.41f9541',
|
||||
'v1.3.27.c88ba58',
|
||||
'v1.3.26.0010231',
|
||||
'v1.3.25.85f46d3',
|
||||
'v1.3.24.dff6915',
|
||||
'v1.3.23.5462d84',
|
||||
'v1.3.22.c725a6b',
|
||||
'v1.3.21.cf00ac5',
|
||||
'v1.3.20.9a5ff93',
|
||||
'v1.3.19.3c6a2f7',
|
||||
'v1.3.17.c9822de',
|
||||
'v1.3.16.97899ae',
|
||||
'v1.3.15.432d067',
|
||||
'v1.3.13.71a43a9',
|
||||
'v1.3.12.6306c53',
|
||||
'v1.3.11.0411401',
|
||||
'v1.3.10.cc2a84a',
|
||||
'v1.3.10.4df0e91',
|
||||
'v1.3.9.92185e7',
|
||||
'v1.3.8.90df7c2',
|
||||
'v1.3.7.bb22b6e',
|
||||
'v1.3.6.f511bab',
|
||||
'v1.3.5.e5b19fd',
|
||||
'v1.3.4.2b20bf3',
|
||||
'v1.3.3.2fe124e',
|
||||
'v1.2.testing1',
|
||||
'v1.2.65.0adc5ce',
|
||||
'v1.2.64.fc48fcd',
|
||||
'v1.2.63.9879494',
|
||||
'v1.2.62.3ddd74e',
|
||||
'v1.2.61.d551c17',
|
||||
'v1.2.60.ab959de',
|
||||
'v1.2.59.d81c1c0',
|
||||
'v1.2.58.6af1822',
|
||||
'v1.2.57.f7c6955',
|
||||
'v1.2.56.596a73c',
|
||||
'v1.2.55.9db7c62',
|
||||
'v1.2.54.288f2be',
|
||||
'v1.2.53.19c1f9f',
|
||||
'v1.2.52.b63802c',
|
||||
'v1.2.51.f9ff06b',
|
||||
'v1.2.50.41dcfdd',
|
||||
'v1.2.49.5354c49',
|
||||
'v1.2.48.371335e',
|
||||
'v1.2.47',
|
||||
'v1.2.46.dce2fe4',
|
||||
'v1.2.46.9d21e58',
|
||||
'v1.2.45.b674054',
|
||||
'v1.2.44.f2c9c55',
|
||||
'v1.2.43.a405d81',
|
||||
'v1.2.42.2759c8d',
|
||||
'v1.2.41.32f3682',
|
||||
'v1.2.39.06892c4',
|
||||
'v1.2.38.cf4e508',
|
||||
'v1.2.38.451b085',
|
||||
'v1.2.36',
|
||||
'v1.2.30.80e4bc6',
|
||||
'v1.2.29.6c95659',
|
||||
"v2.7.16.a597230",
|
||||
"v2.7.15.567b8ea",
|
||||
"v2.7.14.e959000",
|
||||
"v2.7.13.597fa0b",
|
||||
"v2.7.12.45f15b8",
|
||||
"v2.7.11.ee68575",
|
||||
"v2.7.10.94d4bdf",
|
||||
"v2.7.9.70724be",
|
||||
"v2.7.8.a0c0388",
|
||||
"v2.7.7.5ae4ff9",
|
||||
"v2.7.6.834c3c5",
|
||||
"v2.7.5.ddd1499",
|
||||
"v2.7.4.c1f4f79",
|
||||
"v2.7.3.cf574c7",
|
||||
"v2.7.2.f6d3782",
|
||||
"v2.7.1.f35ca81",
|
||||
"v2.7.0.705515a",
|
||||
"v2.7.0.195b7cc",
|
||||
"v2.6.13.0561f2c",
|
||||
"v2.6.12.9861e82",
|
||||
"v2.6.11.60ec05e",
|
||||
"v2.6.10.9ce4455",
|
||||
"v2.6.9.f223b8a",
|
||||
"v2.6.8.ef9d0d7",
|
||||
"v2.6.7.2d6181f",
|
||||
"v2.6.6.54c1423",
|
||||
"v2.6.5.fc3d9f2",
|
||||
"v2.6.4.b89355f",
|
||||
"v2.6.3.d28af68",
|
||||
"v2.6.3.640e731",
|
||||
"v2.6.2.31c0e8f",
|
||||
"v2.6.1.7c3edde",
|
||||
"v2.6.0.f7afa9a",
|
||||
"v2.5.23.bf958ed",
|
||||
"v2.5.22.d1fa27d",
|
||||
"v2.5.21.447533a",
|
||||
"v2.5.20.4c97351",
|
||||
"v2.5.19.f9876cf",
|
||||
"v2.5.19.d5cd6f8",
|
||||
"v2.5.18.89ebafc",
|
||||
"v2.5.17.b4b2fd6",
|
||||
"v2.5.16.f81d3b0",
|
||||
"v2.5.15.79da236",
|
||||
"v2.5.14.f2ee0df",
|
||||
"v2.5.13.295278b",
|
||||
"v2.5.13.1a06f88",
|
||||
"v2.5.12.aa184e6",
|
||||
"v2.5.11.8e2a3e5",
|
||||
"v2.5.10.0fc5c9b",
|
||||
"v2.5.9.936260f",
|
||||
"v2.5.8.6485f03",
|
||||
"v2.5.7.f77c87d",
|
||||
"v2.5.6.d55c08d",
|
||||
"v2.5.5.e182ae7",
|
||||
"v2.5.4.8d288d5",
|
||||
"v2.5.3.a70d5ee",
|
||||
"v2.5.2.771cb52",
|
||||
"v2.5.1.c13b44b",
|
||||
"v2.5.0.e470619",
|
||||
"v2.5.0.d6dac17",
|
||||
"v2.5.0.ab7de7f",
|
||||
"v2.5.0.33eb073",
|
||||
"v2.5.0.9e55e6b",
|
||||
"v2.5.0.9ac0e26",
|
||||
"v2.4.3.efc27f2",
|
||||
"v2.4.3.91d6612",
|
||||
"v2.4.2.5b45303",
|
||||
"v2.4.1.394e0e1",
|
||||
"v2.4.0.46d7b82",
|
||||
"v2.3.15.deb7c27",
|
||||
"v2.3.14.64531fa",
|
||||
"v2.3.13.83f5ba0",
|
||||
"v2.3.12.24458a7",
|
||||
"v2.3.11.2740a56",
|
||||
"v2.3.10.d19607b",
|
||||
"v2.3.9.f06c56a",
|
||||
"v2.3.8.d490a33",
|
||||
"v2.3.7.30fbcab",
|
||||
"v2.3.6.7a3570a",
|
||||
"v2.3.5.2f9b68e",
|
||||
"v2.3.4.ea61808",
|
||||
"v2.3.3.8187fa7",
|
||||
"v2.3.2.63df972",
|
||||
"v2.3.1.4fa7f5a",
|
||||
"v2.3.0.5f47ca1",
|
||||
"v2.2.24.e6a2c06",
|
||||
"v2.2.23.5672e68",
|
||||
"v2.2.22.404d0dd",
|
||||
"v2.2.21.7f7c5cb",
|
||||
"v2.2.20.af5ac32",
|
||||
"v2.2.19.8f6a283",
|
||||
"v2.2.18.e9bde80",
|
||||
"v2.2.17.dbac2b1",
|
||||
"v2.2.16.1c6acfd",
|
||||
"v2.2.15.31c4693",
|
||||
"v2.2.14.57542ce",
|
||||
"v2.2.13.f570204",
|
||||
"v2.2.12.092e6f2",
|
||||
"v2.2.11.10265aa",
|
||||
"v2.2.10.7cebd79",
|
||||
"v2.2.9.47301a5",
|
||||
"v2.2.8.61f6fb2",
|
||||
"v2.2.7.e8970ad",
|
||||
"v2.2.6.b53cb38",
|
||||
"v2.2.5.8255128",
|
||||
"v2.2.4.3bcab0e",
|
||||
"v2.2.3.282cc0b",
|
||||
"v2.2.2.f35c7be",
|
||||
"v2.2.1.fb5f2e4",
|
||||
"v2.2.0.9f6584b",
|
||||
"v2.1.23.04bbdc6",
|
||||
"v2.1.22.191a69d",
|
||||
"v2.1.21.97d7a89",
|
||||
"v2.1.20.470363d",
|
||||
"v2.1.19.eb7025f",
|
||||
"v2.1.18.de53280",
|
||||
"v2.1.17.7ca2e81",
|
||||
"v2.1.16.a2c5b92",
|
||||
"v2.1.15.cd78723",
|
||||
"v2.1.14.99a31c1",
|
||||
"v2.1.13.7475c86",
|
||||
"v2.1.12.7711b03",
|
||||
"v2.1.11.5ec624d",
|
||||
"v2.1.10.7ef12c7",
|
||||
"v2.1.9.d43ddc9",
|
||||
"v2.1.8.ee971e3",
|
||||
"v2.1.7.242f880",
|
||||
"v2.1.6.5679a82",
|
||||
"v2.1.5.23272da",
|
||||
"v2.1.4.958d2cf",
|
||||
"v2.1.3.8c68d88",
|
||||
"v2.1.2.6d20215",
|
||||
"v2.1.1.dc2ca9c",
|
||||
"v2.1.0.331a1af",
|
||||
"v2.0.23.7bb281d",
|
||||
"v2.0.22.fbfd0f1",
|
||||
"v2.0.21.83e6cea",
|
||||
"v2.0.20.7100416",
|
||||
"v2.0.19.3209aea",
|
||||
"v2.0.18.1a7991c",
|
||||
"v2.0.17.5d1c06b",
|
||||
"v2.0.16.2242b68",
|
||||
"v2.0.15.aafbde0",
|
||||
"v2.0.14.2baaad8",
|
||||
"v2.0.13.7e27729",
|
||||
"v2.0.12.2400dd4",
|
||||
"v2.0.11.8914d1a",
|
||||
"v2.0.10.e09b12c",
|
||||
"v2.0.9.6ea0963",
|
||||
"v2.0.8.090e166",
|
||||
"v2.0.7.91ff7b9",
|
||||
"v2.0.6.97fd5cf",
|
||||
"v2.0.5.65e8209",
|
||||
"v2.0.4.5417671",
|
||||
"v2.0.3.09fe616",
|
||||
"v2.0.2.8146e84",
|
||||
"v2.0.1.ad05b91",
|
||||
"v2.0.0.18ab874",
|
||||
"v1.3.48.82bcd39",
|
||||
"v1.3.47.05147c0",
|
||||
"v1.3.46.d4ea956",
|
||||
"v1.3.45.b0d0552",
|
||||
"v1.3.44.4fa8d02",
|
||||
"v1.3.43.aae9d2f",
|
||||
"v1.3.42.9bd9252",
|
||||
"v1.3.41.80ddb81",
|
||||
"v1.3.40.e87ecc2",
|
||||
"v1.3.39.ddc3727",
|
||||
"v1.3.38.1253abd",
|
||||
"v1.3.37.97712a9",
|
||||
"v1.3.36.dd720f2",
|
||||
"v1.3.36.64f852e",
|
||||
"v1.3.36.7e03019",
|
||||
"v1.3.35.3251cd5",
|
||||
"v1.3.34.401b5d9",
|
||||
"v1.3.33.ab0095c",
|
||||
"v1.3.32.7e6c22f",
|
||||
"v1.3.31.0084643",
|
||||
"v1.3.30.9fe2ddb",
|
||||
"v1.3.29.7afc149",
|
||||
"v1.3.28.41f9541",
|
||||
"v1.3.27.c88ba58",
|
||||
"v1.3.26.0010231",
|
||||
"v1.3.25.85f46d3",
|
||||
"v1.3.24.dff6915",
|
||||
"v1.3.23.5462d84",
|
||||
"v1.3.22.c725a6b",
|
||||
"v1.3.21.cf00ac5",
|
||||
"v1.3.20.9a5ff93",
|
||||
"v1.3.19.3c6a2f7",
|
||||
"v1.3.17.c9822de",
|
||||
"v1.3.16.97899ae",
|
||||
"v1.3.15.432d067",
|
||||
"v1.3.13.71a43a9",
|
||||
"v1.3.12.6306c53",
|
||||
"v1.3.11.0411401",
|
||||
"v1.3.10.cc2a84a",
|
||||
"v1.3.10.4df0e91",
|
||||
"v1.3.9.92185e7",
|
||||
"v1.3.8.90df7c2",
|
||||
"v1.3.7.bb22b6e",
|
||||
"v1.3.6.f511bab",
|
||||
"v1.3.5.e5b19fd",
|
||||
"v1.3.4.2b20bf3",
|
||||
"v1.3.3.2fe124e",
|
||||
"v1.2.testing1",
|
||||
"v1.2.65.0adc5ce",
|
||||
"v1.2.64.fc48fcd",
|
||||
"v1.2.63.9879494",
|
||||
"v1.2.62.3ddd74e",
|
||||
"v1.2.61.d551c17",
|
||||
"v1.2.60.ab959de",
|
||||
"v1.2.59.d81c1c0",
|
||||
"v1.2.58.6af1822",
|
||||
"v1.2.57.f7c6955",
|
||||
"v1.2.56.596a73c",
|
||||
"v1.2.55.9db7c62",
|
||||
"v1.2.54.288f2be",
|
||||
"v1.2.53.19c1f9f",
|
||||
"v1.2.52.b63802c",
|
||||
"v1.2.51.f9ff06b",
|
||||
"v1.2.50.41dcfdd",
|
||||
"v1.2.49.5354c49",
|
||||
"v1.2.48.371335e",
|
||||
"v1.2.47",
|
||||
"v1.2.46.dce2fe4",
|
||||
"v1.2.46.9d21e58",
|
||||
"v1.2.45.b674054",
|
||||
"v1.2.44.f2c9c55",
|
||||
"v1.2.43.a405d81",
|
||||
"v1.2.42.2759c8d",
|
||||
"v1.2.41.32f3682",
|
||||
"v1.2.39.06892c4",
|
||||
"v1.2.38.cf4e508",
|
||||
"v1.2.38.451b085",
|
||||
"v1.2.36",
|
||||
"v1.2.30.80e4bc6",
|
||||
"v1.2.29.6c95659",
|
||||
] as const
|
||||
|
||||
export type FirmwareVersion = (typeof VERSIONS)[number]
|
||||
|
||||
@@ -7,8 +7,8 @@ A query function that takes two arguments looks like:
|
||||
|
||||
```ts
|
||||
// convex/myFunctions.ts
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { query } from "./_generated/server"
|
||||
import { v } from "convex/values"
|
||||
|
||||
export const myQueryFunction = query({
|
||||
// Validators for arguments.
|
||||
@@ -21,16 +21,16 @@ export const myQueryFunction = query({
|
||||
handler: async (ctx, args) => {
|
||||
// Read the database as many times as you need here.
|
||||
// See https://docs.convex.dev/database/reading-data.
|
||||
const documents = await ctx.db.query("tablename").collect();
|
||||
const documents = await ctx.db.query("tablename").collect()
|
||||
|
||||
// Arguments passed from the client are properties of the args object.
|
||||
console.log(args.first, args.second);
|
||||
console.log(args.first, args.second)
|
||||
|
||||
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
|
||||
// remove non-public properties, or create new objects.
|
||||
return documents;
|
||||
return documents
|
||||
},
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
Using this query function in a React component looks like:
|
||||
@@ -39,15 +39,15 @@ Using this query function in a React component looks like:
|
||||
const data = useQuery(api.myFunctions.myQueryFunction, {
|
||||
first: 10,
|
||||
second: "hello",
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
A mutation function looks like:
|
||||
|
||||
```ts
|
||||
// convex/myFunctions.ts
|
||||
import { mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { mutation } from "./_generated/server"
|
||||
import { v } from "convex/values"
|
||||
|
||||
export const myMutationFunction = mutation({
|
||||
// Validators for arguments.
|
||||
@@ -61,27 +61,25 @@ export const myMutationFunction = mutation({
|
||||
// Insert or modify documents in the database here.
|
||||
// Mutations can also read from the database like queries.
|
||||
// See https://docs.convex.dev/database/writing-data.
|
||||
const message = { body: args.first, author: args.second };
|
||||
const id = await ctx.db.insert("messages", message);
|
||||
const message = { body: args.first, author: args.second }
|
||||
const id = await ctx.db.insert("messages", message)
|
||||
|
||||
// Optionally, return a value from your mutation.
|
||||
return await ctx.db.get(id);
|
||||
return await ctx.db.get(id)
|
||||
},
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
Using this mutation function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const mutation = useMutation(api.myFunctions.myMutationFunction);
|
||||
const mutation = useMutation(api.myFunctions.myMutationFunction)
|
||||
function handleButtonPress() {
|
||||
// fire and forget, the most common way to use mutations
|
||||
mutation({ first: "Hello!", second: "me" });
|
||||
mutation({ first: "Hello!", second: "me" })
|
||||
// OR
|
||||
// use the result once the mutation has completed
|
||||
mutation({ first: "Hello!", second: "me" }).then((result) =>
|
||||
console.log(result),
|
||||
);
|
||||
mutation({ first: "Hello!", second: "me" }).then(result => console.log(result))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { v } from 'convex/values'
|
||||
import { internal } from './_generated/api'
|
||||
import { action } from './_generated/server'
|
||||
import { v } from "convex/values"
|
||||
import { internal } from "./_generated/api"
|
||||
import { action } from "./_generated/server"
|
||||
|
||||
export const dispatchGithubBuild = action({
|
||||
args: {
|
||||
buildId: v.id('builds'),
|
||||
buildId: v.id("builds"),
|
||||
target: v.string(),
|
||||
flags: v.string(),
|
||||
version: v.string(),
|
||||
@@ -14,52 +14,49 @@ export const dispatchGithubBuild = action({
|
||||
handler: async (ctx, args) => {
|
||||
const githubToken = process.env.GITHUB_TOKEN
|
||||
if (!githubToken) {
|
||||
throw new Error('GITHUB_TOKEN is not set')
|
||||
throw new Error("GITHUB_TOKEN is not set")
|
||||
}
|
||||
|
||||
const convexUrl = process.env.CONVEX_SITE_URL
|
||||
if (!convexUrl) {
|
||||
console.error('CONVEX_SITE_URL is not set')
|
||||
console.error("CONVEX_SITE_URL is not set")
|
||||
// Proceeding anyway might fail if workflow requires it
|
||||
}
|
||||
|
||||
console.log('dispatchGithubBuild args:', JSON.stringify(args, null, 2))
|
||||
console.log("dispatchGithubBuild args:", JSON.stringify(args, null, 2))
|
||||
|
||||
if (!args.buildHash) {
|
||||
throw new Error('args.buildHash is missing or empty')
|
||||
throw new Error("args.buildHash is missing or empty")
|
||||
}
|
||||
|
||||
// Use test workflow when running in Convex dev mode
|
||||
const isDev = process.env.CONVEX_ENV === 'dev'
|
||||
const workflowFile = isDev ? 'custom_build_test.yml' : 'custom_build.yml'
|
||||
const isDev = process.env.CONVEX_ENV === "dev"
|
||||
const workflowFile = isDev ? "custom_build_test.yml" : "custom_build.yml"
|
||||
|
||||
const payload = {
|
||||
ref: 'main', // or make this configurable
|
||||
ref: "main", // or make this configurable
|
||||
inputs: {
|
||||
target: args.target,
|
||||
flags: args.flags,
|
||||
version: args.version,
|
||||
build_id: args.buildId,
|
||||
build_hash: args.buildHash,
|
||||
convex_url: convexUrl || 'https://example.com', // Fallback to avoid missing input error if that's the cause
|
||||
plugins: (args.plugins ?? []).join(' '),
|
||||
convex_url: convexUrl || "https://example.com", // Fallback to avoid missing input error if that's the cause
|
||||
plugins: (args.plugins ?? []).join(" "),
|
||||
},
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Dispatching GitHub build to ${workflowFile} with payload:`,
|
||||
JSON.stringify(payload, null, 2)
|
||||
)
|
||||
console.log(`Dispatching GitHub build to ${workflowFile} with payload:`, JSON.stringify(payload, null, 2))
|
||||
|
||||
try {
|
||||
const url = `https://api.github.com/repos/MeshEnvy/mesh-forge/actions/workflows/${workflowFile}/dispatches`
|
||||
console.log('GitHub API URL:', url)
|
||||
console.log("GitHub API URL:", url)
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { getAuthUserId } from '@convex-dev/auth/server'
|
||||
import { v } from 'convex/values'
|
||||
import { api } from './_generated/api'
|
||||
import { query } from './_generated/server'
|
||||
import { computeFlagsFromConfig } from './builds'
|
||||
import { adminMutation, adminQuery } from './helpers'
|
||||
import { getAuthUserId } from "@convex-dev/auth/server"
|
||||
import { v } from "convex/values"
|
||||
import { api } from "./_generated/api"
|
||||
import { query } from "./_generated/server"
|
||||
import { computeFlagsFromConfig } from "./builds"
|
||||
import { adminMutation, adminQuery } from "./helpers"
|
||||
|
||||
export const isAdmin = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
handler: async ctx => {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) return false
|
||||
|
||||
const userSettings = await ctx.db
|
||||
.query('userSettings')
|
||||
.withIndex('by_user', (q) => q.eq('userId', userId))
|
||||
.query("userSettings")
|
||||
.withIndex("by_user", q => q.eq("userId", userId))
|
||||
.first()
|
||||
|
||||
return userSettings?.isAdmin === true
|
||||
@@ -22,11 +22,11 @@ export const isAdmin = query({
|
||||
|
||||
export const listFailedBuilds = adminQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
handler: async ctx => {
|
||||
const failedBuilds = await ctx.db
|
||||
.query('builds')
|
||||
.filter((q) => q.eq(q.field('status'), 'failure'))
|
||||
.order('desc')
|
||||
.query("builds")
|
||||
.filter(q => q.eq(q.field("status"), "failure"))
|
||||
.order("desc")
|
||||
.collect()
|
||||
|
||||
return failedBuilds
|
||||
@@ -35,8 +35,8 @@ export const listFailedBuilds = adminQuery({
|
||||
|
||||
export const listAllBuilds = adminQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const allBuilds = await ctx.db.query('builds').order('desc').collect()
|
||||
handler: async ctx => {
|
||||
const allBuilds = await ctx.db.query("builds").order("desc").collect()
|
||||
|
||||
return allBuilds
|
||||
},
|
||||
@@ -44,12 +44,12 @@ export const listAllBuilds = adminQuery({
|
||||
|
||||
export const retryBuild = adminMutation({
|
||||
args: {
|
||||
buildId: v.id('builds'),
|
||||
buildId: v.id("builds"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const build = await ctx.db.get(args.buildId)
|
||||
if (!build) {
|
||||
throw new Error('Build not found')
|
||||
throw new Error("Build not found")
|
||||
}
|
||||
|
||||
// Compute flags from config
|
||||
@@ -68,7 +68,7 @@ export const retryBuild = adminMutation({
|
||||
|
||||
// Update build status to queued and clear artifact paths
|
||||
await ctx.db.patch(args.buildId, {
|
||||
status: 'queued',
|
||||
status: "queued",
|
||||
updatedAt: Date.now(),
|
||||
firmwarePath: undefined,
|
||||
sourcePath: undefined,
|
||||
|
||||
@@ -2,7 +2,7 @@ export default {
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.CONVEX_SITE_URL,
|
||||
applicationID: 'convex',
|
||||
applicationID: "convex",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Google from '@auth/core/providers/google'
|
||||
import { convexAuth } from '@convex-dev/auth/server'
|
||||
import Google from "@auth/core/providers/google"
|
||||
import { convexAuth } from "@convex-dev/auth/server"
|
||||
|
||||
export const { auth, signIn, signOut, store } = convexAuth({
|
||||
providers: [Google],
|
||||
|
||||
153
convex/builds.ts
153
convex/builds.ts
@@ -1,14 +1,14 @@
|
||||
import { v } from 'convex/values'
|
||||
import { pick } from 'convex-helpers'
|
||||
import { api, internal } from './_generated/api'
|
||||
import type { Doc, Id } from './_generated/dataModel'
|
||||
import { internalMutation, mutation, query } from './_generated/server'
|
||||
import { generateSignedDownloadUrl } from './lib/r2'
|
||||
import { buildFields } from './schema'
|
||||
import { pick } from "convex-helpers"
|
||||
import { v } from "convex/values"
|
||||
import { api, internal } from "./_generated/api"
|
||||
import type { Doc, Id } from "./_generated/dataModel"
|
||||
import { internalMutation, mutation, query } from "./_generated/server"
|
||||
import { generateSignedDownloadUrl } from "./lib/r2"
|
||||
import { buildFields } from "./schema"
|
||||
|
||||
export enum ArtifactType {
|
||||
Firmware = 'firmware',
|
||||
Source = 'source',
|
||||
Firmware = "firmware",
|
||||
Source = "source",
|
||||
}
|
||||
|
||||
type BuildUpdateData = {
|
||||
@@ -17,7 +17,7 @@ type BuildUpdateData = {
|
||||
}
|
||||
|
||||
export const get = query({
|
||||
args: { id: v.id('builds') },
|
||||
args: { id: v.id("builds") },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id)
|
||||
},
|
||||
@@ -27,8 +27,8 @@ export const getByHash = query({
|
||||
args: { buildHash: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const build = await ctx.db
|
||||
.query('builds')
|
||||
.filter((q) => q.eq(q.field('buildHash'), args.buildHash))
|
||||
.query("builds")
|
||||
.filter(q => q.eq(q.field("buildHash"), args.buildHash))
|
||||
.unique()
|
||||
return build ?? null
|
||||
},
|
||||
@@ -38,15 +38,13 @@ export const getByHash = query({
|
||||
* Computes flags string from build config.
|
||||
* Only excludes modules explicitly marked as excluded (config[id] === true).
|
||||
*/
|
||||
export function computeFlagsFromConfig(
|
||||
config: Doc<'builds'>['config']
|
||||
): string {
|
||||
export function computeFlagsFromConfig(config: Doc<"builds">["config"]): string {
|
||||
// Sort modules to ensure consistent order
|
||||
return Object.keys(config.modulesExcluded)
|
||||
.sort()
|
||||
.filter((module) => config.modulesExcluded[module])
|
||||
.filter(module => config.modulesExcluded[module])
|
||||
.map((moduleExcludedName: string) => `-D${moduleExcludedName}=1`)
|
||||
.join(' ')
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +52,7 @@ export function computeFlagsFromConfig(
|
||||
* Uses characters: 0-9, a-z, A-Z (62 characters total)
|
||||
*/
|
||||
function base62Encode(bytes: Uint8Array): string {
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
// Convert bytes to a big-endian number
|
||||
let num = BigInt(0)
|
||||
@@ -63,7 +61,7 @@ function base62Encode(bytes: Uint8Array): string {
|
||||
}
|
||||
|
||||
// Convert number to base62
|
||||
if (num === BigInt(0)) return '0'
|
||||
if (num === BigInt(0)) return "0"
|
||||
|
||||
const result: string[] = []
|
||||
while (num > BigInt(0)) {
|
||||
@@ -71,7 +69,7 @@ function base62Encode(bytes: Uint8Array): string {
|
||||
num = num / BigInt(62)
|
||||
}
|
||||
|
||||
return result.reverse().join('')
|
||||
return result.reverse().join("")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +95,7 @@ async function computeBuildHashInternal(
|
||||
// Use Web Crypto API for SHA-256 hashing
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(input)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
||||
const hashBytes = new Uint8Array(hashBuffer)
|
||||
|
||||
// Encode to base62 instead of hex
|
||||
@@ -108,17 +106,10 @@ async function computeBuildHashInternal(
|
||||
* Computes buildHash from build config.
|
||||
* This is the single source of truth for build hash computation.
|
||||
*/
|
||||
export async function computeBuildHash(
|
||||
config: Doc<'builds'>['config']
|
||||
): Promise<{ hash: string; flags: string }> {
|
||||
export async function computeBuildHash(config: Doc<"builds">["config"]): Promise<{ hash: string; flags: string }> {
|
||||
const flags = computeFlagsFromConfig(config)
|
||||
const plugins = config.pluginsEnabled ?? []
|
||||
const hash = await computeBuildHashInternal(
|
||||
config.version,
|
||||
config.target,
|
||||
flags,
|
||||
plugins
|
||||
)
|
||||
const hash = await computeBuildHashInternal(config.version, config.target, flags, plugins)
|
||||
return { hash, flags }
|
||||
}
|
||||
|
||||
@@ -127,18 +118,17 @@ export async function computeBuildHash(
|
||||
* Uses {artifactType}-<buildHash>-<githubRunId>.tar.gz format.
|
||||
*/
|
||||
export function getR2ArtifactUrl(
|
||||
build: Pick<Doc<'builds'>, 'buildHash' | 'githubRunId'>,
|
||||
build: Pick<Doc<"builds">, "buildHash" | "githubRunId">,
|
||||
artifactType: ArtifactType
|
||||
): string {
|
||||
const r2PublicUrl = process.env.R2_PUBLIC_URL
|
||||
if (!r2PublicUrl) {
|
||||
throw new Error('R2_PUBLIC_URL is not set')
|
||||
throw new Error("R2_PUBLIC_URL is not set")
|
||||
}
|
||||
if (!build.githubRunId) {
|
||||
throw new Error('githubRunId is required to construct artifact URL')
|
||||
throw new Error("githubRunId is required to construct artifact URL")
|
||||
}
|
||||
const artifactTypeStr =
|
||||
artifactType === ArtifactType.Source ? 'source' : 'firmware'
|
||||
const artifactTypeStr = artifactType === ArtifactType.Source ? "source" : "firmware"
|
||||
const path = `/${artifactTypeStr}-${build.buildHash}-${build.githubRunId}.tar.gz`
|
||||
return `${r2PublicUrl}${path}`
|
||||
}
|
||||
@@ -147,7 +137,7 @@ export function getR2ArtifactUrl(
|
||||
// This is the single source of truth for build creation
|
||||
export const upsertBuild = internalMutation({
|
||||
args: {
|
||||
...pick(buildFields, ['buildHash', 'config']),
|
||||
...pick(buildFields, ["buildHash", "config"]),
|
||||
status: v.optional(v.string()),
|
||||
flags: v.string(),
|
||||
},
|
||||
@@ -155,8 +145,8 @@ export const upsertBuild = internalMutation({
|
||||
handler: async (ctx, args) => {
|
||||
// Check if build already exists with this hash
|
||||
const existingBuild = await ctx.db
|
||||
.query('builds')
|
||||
.filter((q) => q.eq(q.field('buildHash'), args.buildHash))
|
||||
.query("builds")
|
||||
.filter(q => q.eq(q.field("buildHash"), args.buildHash))
|
||||
.unique()
|
||||
|
||||
const { status, buildHash, config, flags } = args
|
||||
@@ -170,8 +160,8 @@ export const upsertBuild = internalMutation({
|
||||
}
|
||||
|
||||
// Create new build (artifact paths are omitted, will be undefined)
|
||||
const buildId = await ctx.db.insert('builds', {
|
||||
status: 'queued',
|
||||
const buildId = await ctx.db.insert("builds", {
|
||||
status: "queued",
|
||||
startedAt: Date.now(),
|
||||
buildHash,
|
||||
updatedAt: Date.now(),
|
||||
@@ -203,7 +193,7 @@ export const ensureBuildFromConfig = mutation({
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Construct config for the build
|
||||
const config: Doc<'builds'>['config'] = {
|
||||
const config: Doc<"builds">["config"] = {
|
||||
version: args.version,
|
||||
modulesExcluded: args.modulesExcluded ?? {},
|
||||
target: args.target,
|
||||
@@ -214,8 +204,8 @@ export const ensureBuildFromConfig = mutation({
|
||||
const { hash: buildHash, flags } = await computeBuildHash(config)
|
||||
|
||||
const existingBuild = await ctx.db
|
||||
.query('builds')
|
||||
.filter((q) => q.eq(q.field('buildHash'), buildHash))
|
||||
.query("builds")
|
||||
.filter(q => q.eq(q.field("buildHash"), buildHash))
|
||||
.unique()
|
||||
|
||||
if (existingBuild) {
|
||||
@@ -226,14 +216,11 @@ export const ensureBuildFromConfig = mutation({
|
||||
}
|
||||
}
|
||||
|
||||
const buildId: Id<'builds'> = await ctx.runMutation(
|
||||
internal.builds.upsertBuild,
|
||||
{
|
||||
buildHash,
|
||||
flags,
|
||||
config,
|
||||
}
|
||||
)
|
||||
const buildId: Id<"builds"> = await ctx.runMutation(internal.builds.upsertBuild, {
|
||||
buildHash,
|
||||
flags,
|
||||
config,
|
||||
})
|
||||
|
||||
return {
|
||||
buildId,
|
||||
@@ -245,7 +232,7 @@ export const ensureBuildFromConfig = mutation({
|
||||
|
||||
// Internal query to get build without auth checks (for webhooks)
|
||||
export const getInternal = internalMutation({
|
||||
args: { buildId: v.id('builds') },
|
||||
args: { buildId: v.id("builds") },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.buildId)
|
||||
},
|
||||
@@ -254,12 +241,12 @@ export const getInternal = internalMutation({
|
||||
// Internal mutation to log errors from actions
|
||||
export const logBuildError = internalMutation({
|
||||
args: {
|
||||
buildId: v.id('builds'),
|
||||
buildId: v.id("builds"),
|
||||
error: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.buildId, {
|
||||
status: 'failure',
|
||||
status: "failure",
|
||||
completedAt: Date.now(),
|
||||
})
|
||||
},
|
||||
@@ -268,14 +255,8 @@ export const logBuildError = internalMutation({
|
||||
// Internal mutation to update build status
|
||||
export const updateBuildStatus = internalMutation({
|
||||
args: {
|
||||
...pick(buildFields, [
|
||||
'status',
|
||||
'completedAt',
|
||||
'githubRunId',
|
||||
'firmwarePath',
|
||||
'sourcePath',
|
||||
]),
|
||||
buildId: v.id('builds'),
|
||||
...pick(buildFields, ["status", "completedAt", "githubRunId", "firmwarePath", "sourcePath"]),
|
||||
buildId: v.id("builds"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const build = await ctx.db.get(args.buildId)
|
||||
@@ -291,12 +272,12 @@ export const updateBuildStatus = internalMutation({
|
||||
}
|
||||
|
||||
// Only set completedAt for final statuses
|
||||
if (args.status === 'success' || args.status === 'failure') {
|
||||
if (args.status === "success" || args.status === "failure") {
|
||||
updateData.completedAt = Date.now()
|
||||
}
|
||||
|
||||
// Clear artifact paths when build starts (queued status)
|
||||
if (args.status === 'queued') {
|
||||
if (args.status === "queued") {
|
||||
updateData.firmwarePath = undefined
|
||||
updateData.sourcePath = undefined
|
||||
}
|
||||
@@ -327,9 +308,7 @@ export const updateBuildStatus = internalMutation({
|
||||
updateData.githubRunId = args.githubRunId
|
||||
}
|
||||
|
||||
updateData.githubRunIdHistory = [...new Set(existingHistory)].filter(
|
||||
(id) => id !== args.githubRunId
|
||||
)
|
||||
updateData.githubRunIdHistory = [...new Set(existingHistory)].filter(id => id !== args.githubRunId)
|
||||
|
||||
await ctx.db.patch(args.buildId, updateData)
|
||||
},
|
||||
@@ -337,34 +316,28 @@ export const updateBuildStatus = internalMutation({
|
||||
|
||||
export const generateDownloadUrl = mutation({
|
||||
args: {
|
||||
buildId: v.id('builds'),
|
||||
artifactType: v.union(v.literal('firmware'), v.literal('source')),
|
||||
profileId: v.optional(v.id('profiles')),
|
||||
buildId: v.id("builds"),
|
||||
artifactType: v.union(v.literal("firmware"), v.literal("source")),
|
||||
profileId: v.optional(v.id("profiles")),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const build = await ctx.db.get(args.buildId)
|
||||
if (!build) throw new Error('Build not found')
|
||||
if (!build) throw new Error("Build not found")
|
||||
|
||||
if (!build.githubRunId) {
|
||||
throw new Error('Build githubRunId is required for download')
|
||||
throw new Error("Build githubRunId is required for download")
|
||||
}
|
||||
|
||||
const artifactTypeEnum =
|
||||
args.artifactType === 'source'
|
||||
? ArtifactType.Source
|
||||
: ArtifactType.Firmware
|
||||
const artifactTypeEnum = args.artifactType === "source" ? ArtifactType.Source : ArtifactType.Firmware
|
||||
|
||||
const isSource = artifactTypeEnum === ArtifactType.Source
|
||||
const artifactTypeStr =
|
||||
artifactTypeEnum === ArtifactType.Source ? 'source' : 'firmware'
|
||||
const contentType = isSource
|
||||
? 'application/gzip'
|
||||
: 'application/octet-stream'
|
||||
const artifactTypeStr = artifactTypeEnum === ArtifactType.Source ? "source" : "firmware"
|
||||
const contentType = isSource ? "application/gzip" : "application/octet-stream"
|
||||
|
||||
// Use stored path if available, otherwise construct from buildHash and githubRunId
|
||||
const storedPath = isSource ? build.sourcePath : build.firmwarePath
|
||||
const objectKey = storedPath
|
||||
? storedPath.startsWith('/')
|
||||
? storedPath.startsWith("/")
|
||||
? storedPath.slice(1)
|
||||
: storedPath
|
||||
: `${artifactTypeStr}-${build.buildHash}-${build.githubRunId}.tar.gz`
|
||||
@@ -373,7 +346,7 @@ export const generateDownloadUrl = mutation({
|
||||
const profile = await (async () => {
|
||||
if (!args.profileId) return
|
||||
const profileDoc = await ctx.db.get(args.profileId)
|
||||
if (!profileDoc) throw new Error('Profile not found')
|
||||
if (!profileDoc) throw new Error("Profile not found")
|
||||
return profileDoc
|
||||
})()
|
||||
|
||||
@@ -381,9 +354,9 @@ export const generateDownloadUrl = mutation({
|
||||
const profileSlug = profile
|
||||
? profile.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)+/g, '')
|
||||
: ''
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, "")
|
||||
: ""
|
||||
|
||||
// Increment profile flash count for firmware downloads
|
||||
if (profile && !isSource) {
|
||||
@@ -395,18 +368,14 @@ export const generateDownloadUrl = mutation({
|
||||
}
|
||||
|
||||
// Increment plugin flash counts for firmware downloads (independent of profile)
|
||||
if (
|
||||
!isSource &&
|
||||
build.config.pluginsEnabled &&
|
||||
build.config.pluginsEnabled.length > 0
|
||||
) {
|
||||
if (!isSource && build.config.pluginsEnabled && build.config.pluginsEnabled.length > 0) {
|
||||
await ctx.runMutation(internal.plugins.incrementFlashCount, {
|
||||
slugs: build.config.pluginsEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
const last4Hash = build.buildHash.slice(-4)
|
||||
const os = 'meshtastic' // OS/platform identifier
|
||||
const os = "meshtastic" // OS/platform identifier
|
||||
const version = build.config.version
|
||||
const target = build.config.target
|
||||
const jobId = build.githubRunId
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { getAuthUserId } from '@convex-dev/auth/server'
|
||||
import { ConvexError, v } from 'convex/values'
|
||||
import {
|
||||
customAction,
|
||||
customCtx,
|
||||
customMutation,
|
||||
customQuery,
|
||||
} from 'convex-helpers/server/customFunctions'
|
||||
import { api } from './_generated/api'
|
||||
import { action, mutation, query } from './_generated/server'
|
||||
import { getAuthUserId } from "@convex-dev/auth/server"
|
||||
import { customAction, customCtx, customMutation, customQuery } from "convex-helpers/server/customFunctions"
|
||||
import { ConvexError, v } from "convex/values"
|
||||
import { api } from "./_generated/api"
|
||||
import { action, mutation, query } from "./_generated/server"
|
||||
|
||||
export const authQuery = customQuery(
|
||||
query,
|
||||
customCtx(async (ctx) => {
|
||||
customCtx(async ctx => {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
if (identity === null) {
|
||||
throw new ConvexError('Not authenticated!')
|
||||
throw new ConvexError("Not authenticated!")
|
||||
}
|
||||
return {}
|
||||
})
|
||||
@@ -22,10 +17,10 @@ export const authQuery = customQuery(
|
||||
|
||||
export const authMutation = customMutation(
|
||||
mutation,
|
||||
customCtx(async (ctx) => {
|
||||
customCtx(async ctx => {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
if (identity === null) {
|
||||
throw new ConvexError('Not authenticated!')
|
||||
throw new ConvexError("Not authenticated!")
|
||||
}
|
||||
return {}
|
||||
})
|
||||
@@ -33,10 +28,10 @@ export const authMutation = customMutation(
|
||||
|
||||
export const authAction = customAction(
|
||||
action,
|
||||
customCtx(async (ctx) => {
|
||||
customCtx(async ctx => {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
if (identity === null) {
|
||||
throw new ConvexError('Not authenticated!')
|
||||
throw new ConvexError("Not authenticated!")
|
||||
}
|
||||
return {}
|
||||
})
|
||||
@@ -44,19 +39,19 @@ export const authAction = customAction(
|
||||
|
||||
export const adminQuery = customQuery(
|
||||
query,
|
||||
customCtx(async (ctx) => {
|
||||
customCtx(async ctx => {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) {
|
||||
throw new ConvexError('Not authenticated!')
|
||||
throw new ConvexError("Not authenticated!")
|
||||
}
|
||||
|
||||
const userSettings = await ctx.db
|
||||
.query('userSettings')
|
||||
.withIndex('by_user', (q) => q.eq('userId', userId))
|
||||
.query("userSettings")
|
||||
.withIndex("by_user", q => q.eq("userId", userId))
|
||||
.first()
|
||||
|
||||
if (userSettings?.isAdmin !== true) {
|
||||
throw new ConvexError('Unauthorized: Admin access required')
|
||||
throw new ConvexError("Unauthorized: Admin access required")
|
||||
}
|
||||
|
||||
return {}
|
||||
@@ -65,19 +60,19 @@ export const adminQuery = customQuery(
|
||||
|
||||
export const adminMutation = customMutation(
|
||||
mutation,
|
||||
customCtx(async (ctx) => {
|
||||
customCtx(async ctx => {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) {
|
||||
throw new ConvexError('Not authenticated!')
|
||||
throw new ConvexError("Not authenticated!")
|
||||
}
|
||||
|
||||
const userSettings = await ctx.db
|
||||
.query('userSettings')
|
||||
.withIndex('by_user', (q) => q.eq('userId', userId))
|
||||
.query("userSettings")
|
||||
.withIndex("by_user", q => q.eq("userId", userId))
|
||||
.first()
|
||||
|
||||
if (userSettings?.isAdmin !== true) {
|
||||
throw new ConvexError('Unauthorized: Admin access required')
|
||||
throw new ConvexError("Unauthorized: Admin access required")
|
||||
}
|
||||
|
||||
return {}
|
||||
@@ -86,10 +81,10 @@ export const adminMutation = customMutation(
|
||||
|
||||
export const adminAction = customAction(
|
||||
action,
|
||||
customCtx(async (ctx) => {
|
||||
customCtx(async ctx => {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) {
|
||||
throw new ConvexError('Not authenticated!')
|
||||
throw new ConvexError("Not authenticated!")
|
||||
}
|
||||
|
||||
// Actions can't access ctx.db directly, so we need to use a query
|
||||
@@ -97,7 +92,7 @@ export const adminAction = customAction(
|
||||
userId,
|
||||
})
|
||||
if (!isAdmin) {
|
||||
throw new ConvexError('Unauthorized: Admin access required')
|
||||
throw new ConvexError("Unauthorized: Admin access required")
|
||||
}
|
||||
|
||||
return {}
|
||||
@@ -106,11 +101,11 @@ export const adminAction = customAction(
|
||||
|
||||
// Internal query to check if user is admin (used by action middleware)
|
||||
export const checkIsAdmin = query({
|
||||
args: { userId: v.id('users') },
|
||||
args: { userId: v.id("users") },
|
||||
handler: async (ctx, args) => {
|
||||
const userSettings = await ctx.db
|
||||
.query('userSettings')
|
||||
.withIndex('by_user', (q) => q.eq('userId', args.userId))
|
||||
.query("userSettings")
|
||||
.withIndex("by_user", q => q.eq("userId", args.userId))
|
||||
.first()
|
||||
|
||||
return userSettings?.isAdmin === true
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { httpRouter } from 'convex/server'
|
||||
import { internal } from './_generated/api'
|
||||
import { httpAction } from './_generated/server'
|
||||
import { auth } from './auth'
|
||||
import { httpRouter } from "convex/server"
|
||||
import { internal } from "./_generated/api"
|
||||
import { httpAction } from "./_generated/server"
|
||||
import { auth } from "./auth"
|
||||
|
||||
const http = httpRouter()
|
||||
|
||||
auth.addHttpRoutes(http)
|
||||
|
||||
http.route({
|
||||
path: '/github-webhook',
|
||||
method: 'POST',
|
||||
path: "/github-webhook",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
// Verify bearer token
|
||||
const buildToken = process.env.CONVEX_BUILD_TOKEN
|
||||
if (!buildToken) {
|
||||
return new Response('CONVEX_BUILD_TOKEN not configured', { status: 500 })
|
||||
return new Response("CONVEX_BUILD_TOKEN not configured", { status: 500 })
|
||||
}
|
||||
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return new Response('Missing or invalid Authorization header', {
|
||||
const authHeader = request.headers.get("Authorization")
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return new Response("Missing or invalid Authorization header", {
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7) // Remove 'Bearer ' prefix
|
||||
if (token !== buildToken) {
|
||||
return new Response('Invalid token', { status: 401 })
|
||||
return new Response("Invalid token", { status: 401 })
|
||||
}
|
||||
|
||||
const payload = await request.json()
|
||||
|
||||
// Validate build_id and state are present
|
||||
if (!payload.build_id || !payload.state) {
|
||||
return new Response('Missing build_id or state', { status: 400 })
|
||||
return new Response("Missing build_id or state", { status: 400 })
|
||||
}
|
||||
|
||||
// Verify build exists
|
||||
@@ -42,12 +42,10 @@ http.route({
|
||||
})
|
||||
|
||||
if (!build) {
|
||||
return new Response('Build not found', { status: 404 })
|
||||
return new Response("Build not found", { status: 404 })
|
||||
}
|
||||
|
||||
const githubRunId = payload.github_run_id
|
||||
? Number(payload.github_run_id)
|
||||
: undefined
|
||||
const githubRunId = payload.github_run_id ? Number(payload.github_run_id) : undefined
|
||||
|
||||
await ctx.runMutation(internal.builds.updateBuildStatus, {
|
||||
buildId: payload.build_id,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||||
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||
|
||||
export async function generateSignedDownloadUrl(
|
||||
objectKey: string,
|
||||
filename: string,
|
||||
contentType: string = 'application/octet-stream'
|
||||
contentType: string = "application/octet-stream"
|
||||
): Promise<string> {
|
||||
const accountId = process.env.R2_ACCOUNT_ID
|
||||
const accessKeyId = process.env.R2_ACCESS_KEY_ID
|
||||
@@ -12,11 +12,11 @@ export async function generateSignedDownloadUrl(
|
||||
const bucketName = process.env.R2_BUCKET_NAME
|
||||
|
||||
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {
|
||||
throw new Error('R2 credentials are not set')
|
||||
throw new Error("R2 credentials are not set")
|
||||
}
|
||||
|
||||
const s3 = new S3Client({
|
||||
region: 'auto',
|
||||
region: "auto",
|
||||
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { v } from 'convex/values'
|
||||
import { internalMutation, query } from './_generated/server'
|
||||
import { v } from "convex/values"
|
||||
import { internalMutation, query } from "./_generated/server"
|
||||
|
||||
export const get = query({
|
||||
args: { slug: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const plugin = await ctx.db
|
||||
.query('plugins')
|
||||
.withIndex('by_slug', (q) => q.eq('slug', args.slug))
|
||||
.query("plugins")
|
||||
.withIndex("by_slug", q => q.eq("slug", args.slug))
|
||||
.unique()
|
||||
return plugin
|
||||
? { slug: plugin.slug, flashCount: plugin.flashCount }
|
||||
: { slug: args.slug, flashCount: 0 }
|
||||
return plugin ? { slug: plugin.slug, flashCount: plugin.flashCount } : { slug: args.slug, flashCount: 0 }
|
||||
},
|
||||
})
|
||||
|
||||
export const getAll = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const plugins = await ctx.db.query('plugins').collect()
|
||||
handler: async ctx => {
|
||||
const plugins = await ctx.db.query("plugins").collect()
|
||||
const counts: Record<string, number> = {}
|
||||
for (const plugin of plugins) {
|
||||
counts[plugin.slug] = plugin.flashCount
|
||||
@@ -31,11 +29,11 @@ export const incrementFlashCount = internalMutation({
|
||||
handler: async (ctx, args) => {
|
||||
for (const pluginSpec of args.slugs) {
|
||||
// Extract slug from "slug@version" format, or use as-is if no @ present
|
||||
const slug = pluginSpec.split('@')[0]
|
||||
const slug = pluginSpec.split("@")[0]
|
||||
|
||||
const existing = await ctx.db
|
||||
.query('plugins')
|
||||
.withIndex('by_slug', (q) => q.eq('slug', slug))
|
||||
.query("plugins")
|
||||
.withIndex("by_slug", q => q.eq("slug", slug))
|
||||
.unique()
|
||||
|
||||
if (existing) {
|
||||
@@ -44,7 +42,7 @@ export const incrementFlashCount = internalMutation({
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
} else {
|
||||
await ctx.db.insert('plugins', {
|
||||
await ctx.db.insert("plugins", {
|
||||
slug,
|
||||
flashCount: 1,
|
||||
updatedAt: Date.now(),
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { getAuthUserId } from '@convex-dev/auth/server'
|
||||
import { v } from 'convex/values'
|
||||
import { internalMutation, mutation, query } from './_generated/server'
|
||||
import { buildConfigFields } from './schema'
|
||||
import { getAuthUserId } from "@convex-dev/auth/server"
|
||||
import { v } from "convex/values"
|
||||
import { internalMutation, mutation, query } from "./_generated/server"
|
||||
import { buildConfigFields } from "./schema"
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
handler: async ctx => {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) return []
|
||||
|
||||
return await ctx.db
|
||||
.query('profiles')
|
||||
.filter((q) => q.eq(q.field('userId'), userId))
|
||||
.query("profiles")
|
||||
.filter(q => q.eq(q.field("userId"), userId))
|
||||
.collect()
|
||||
},
|
||||
})
|
||||
|
||||
export const listPublic = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
handler: async ctx => {
|
||||
const allProfiles = await ctx.db
|
||||
.query('profiles')
|
||||
.filter((q) => q.eq(q.field('isPublic'), true))
|
||||
.query("profiles")
|
||||
.filter(q => q.eq(q.field("isPublic"), true))
|
||||
.collect()
|
||||
return allProfiles.sort((a, b) => (b.flashCount ?? 0) - (a.flashCount ?? 0))
|
||||
},
|
||||
})
|
||||
|
||||
export const get = query({
|
||||
args: { id: v.id('profiles') },
|
||||
args: { id: v.id("profiles") },
|
||||
handler: async (ctx, args) => {
|
||||
const profile = await ctx.db.get(args.id)
|
||||
if (!profile) return null
|
||||
@@ -46,7 +46,7 @@ export const get = query({
|
||||
|
||||
// Internal mutation to get a build by ID
|
||||
export const getBuildById = internalMutation({
|
||||
args: { buildId: v.id('builds') },
|
||||
args: { buildId: v.id("builds") },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.buildId)
|
||||
},
|
||||
@@ -54,12 +54,12 @@ export const getBuildById = internalMutation({
|
||||
|
||||
export const recordFlash = mutation({
|
||||
args: {
|
||||
profileId: v.id('profiles'),
|
||||
profileId: v.id("profiles"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const profile = await ctx.db.get(args.profileId)
|
||||
if (!profile) {
|
||||
throw new Error('Profile not found')
|
||||
throw new Error("Profile not found")
|
||||
}
|
||||
|
||||
const nextCount = (profile.flashCount ?? 0) + 1
|
||||
@@ -75,7 +75,7 @@ export const recordFlash = mutation({
|
||||
|
||||
export const upsert = mutation({
|
||||
args: {
|
||||
id: v.optional(v.id('profiles')),
|
||||
id: v.optional(v.id("profiles")),
|
||||
name: v.string(),
|
||||
description: v.string(),
|
||||
config: v.object(buildConfigFields),
|
||||
@@ -83,13 +83,13 @@ export const upsert = mutation({
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) throw new Error('Unauthorized')
|
||||
if (!userId) throw new Error("Unauthorized")
|
||||
|
||||
if (args.id) {
|
||||
// Update existing profile
|
||||
const profile = await ctx.db.get(args.id)
|
||||
if (!profile || profile.userId !== userId) {
|
||||
throw new Error('Unauthorized')
|
||||
throw new Error("Unauthorized")
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
@@ -104,7 +104,7 @@ export const upsert = mutation({
|
||||
return args.id
|
||||
} else {
|
||||
// Create new profile
|
||||
const profileId = await ctx.db.insert('profiles', {
|
||||
const profileId = await ctx.db.insert("profiles", {
|
||||
userId,
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
@@ -120,14 +120,14 @@ export const upsert = mutation({
|
||||
})
|
||||
|
||||
export const remove = mutation({
|
||||
args: { id: v.id('profiles') },
|
||||
args: { id: v.id("profiles") },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx)
|
||||
if (!userId) throw new Error('Unauthorized')
|
||||
if (!userId) throw new Error("Unauthorized")
|
||||
|
||||
const profile = await ctx.db.get(args.id)
|
||||
if (!profile || profile.userId !== userId) {
|
||||
throw new Error('Unauthorized')
|
||||
throw new Error("Unauthorized")
|
||||
}
|
||||
|
||||
await ctx.db.delete(args.id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { authTables } from '@convex-dev/auth/server'
|
||||
import { defineSchema, defineTable } from 'convex/server'
|
||||
import { v } from 'convex/values'
|
||||
import { authTables } from "@convex-dev/auth/server"
|
||||
import { defineSchema, defineTable } from "convex/server"
|
||||
import { v } from "convex/values"
|
||||
|
||||
export const buildConfigFields = {
|
||||
version: v.string(),
|
||||
@@ -10,7 +10,7 @@ export const buildConfigFields = {
|
||||
}
|
||||
|
||||
export const profileFields = {
|
||||
userId: v.id('users'),
|
||||
userId: v.id("users"),
|
||||
name: v.string(),
|
||||
description: v.string(),
|
||||
config: v.object(buildConfigFields),
|
||||
@@ -43,7 +43,7 @@ export const pluginFields = {
|
||||
}
|
||||
|
||||
export const userSettingsFields = {
|
||||
userId: v.id('users'),
|
||||
userId: v.id("users"),
|
||||
isAdmin: v.boolean(),
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ export const schema = defineSchema({
|
||||
...authTables,
|
||||
profiles: defineTable(profileFields),
|
||||
builds: defineTable(buildFields),
|
||||
plugins: defineTable(pluginFields).index('by_slug', ['slug']),
|
||||
userSettings: defineTable(userSettingsFields).index('by_user', ['userId']),
|
||||
plugins: defineTable(pluginFields).index("by_slug", ["slug"]),
|
||||
userSettings: defineTable(userSettingsFields).index("by_user", ["userId"]),
|
||||
})
|
||||
|
||||
export const buildsDocValidator = schema.tables.builds.validator
|
||||
|
||||
80
lib/utils.ts
80
lib/utils.ts
@@ -1,6 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import PARENT_MAP from '../constants/architecture-hierarchy.json'
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import PARENT_MAP from "../constants/architecture-hierarchy.json"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@@ -34,16 +34,16 @@ export function timeAgo(date: number | string | Date): string {
|
||||
|
||||
export function humanizeStatus(status: string): string {
|
||||
// Handle special statuses
|
||||
if (status === 'success') return 'Success'
|
||||
if (status === 'failure') return 'Failure'
|
||||
if (status === 'queued') return 'Queued'
|
||||
if (status === 'in_progress') return 'In Progress'
|
||||
if (status === "success") return "Success"
|
||||
if (status === "failure") return "Failure"
|
||||
if (status === "queued") return "Queued"
|
||||
if (status === "in_progress") return "In Progress"
|
||||
|
||||
// Convert snake_case/underscore_separated to Title Case
|
||||
return status
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ')
|
||||
.split("_")
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ export function getDependedPlugins(
|
||||
// Process each dependency
|
||||
for (const [depId] of Object.entries(plugin.dependencies)) {
|
||||
// Skip "meshtastic" - it's a firmware version requirement, not a plugin
|
||||
if (depId === 'meshtastic') continue
|
||||
if (depId === "meshtastic") continue
|
||||
|
||||
// Only include dependencies that exist in the registry
|
||||
if (depId in registry) {
|
||||
@@ -101,7 +101,7 @@ export function getImplicitDependencies(
|
||||
): Set<string> {
|
||||
const allDependencies = getDependedPlugins(explicitlySelectedPlugins, registry)
|
||||
const explicitSet = new Set(explicitlySelectedPlugins)
|
||||
return new Set(allDependencies.filter((id) => !explicitSet.has(id)))
|
||||
return new Set(allDependencies.filter(id => !explicitSet.has(id)))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,14 +116,14 @@ export function isRequiredByOther(
|
||||
// Check if any explicitly selected plugin depends on this plugin
|
||||
for (const selectedId of explicitlySelectedPlugins) {
|
||||
if (selectedId === pluginId) continue // Skip self
|
||||
|
||||
|
||||
// Get all dependencies (including transitive) of this selected plugin
|
||||
const allDeps = getDependedPlugins([selectedId], registry)
|
||||
if (allDeps.includes(pluginId)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function isRequiredByOther(
|
||||
* Some sources might use "esp32_s3" (with underscores)
|
||||
*/
|
||||
function normalizeArchitecture(arch: string): string {
|
||||
return arch.replace(/[-_]/g, '')
|
||||
return arch.replace(/[-_]/g, "")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,27 +144,27 @@ function normalizeArchitecture(arch: string): string {
|
||||
export function getBaseArchitecture(name: string): string | null {
|
||||
const normalized = normalizeArchitecture(name)
|
||||
const parentMap = PARENT_MAP as Record<string, string | null>
|
||||
|
||||
|
||||
const visited = new Set<string>()
|
||||
let current = normalized
|
||||
|
||||
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current)
|
||||
const parent = parentMap[current]
|
||||
|
||||
|
||||
// If parent is null, we've reached a base architecture
|
||||
if (parent === null) {
|
||||
return current
|
||||
}
|
||||
|
||||
|
||||
// If no parent found, return current (might be unknown)
|
||||
if (parent === undefined) {
|
||||
return current
|
||||
}
|
||||
|
||||
|
||||
current = normalizeArchitecture(parent)
|
||||
}
|
||||
|
||||
|
||||
// Circular reference or unknown, return the last known
|
||||
return current || normalized
|
||||
}
|
||||
@@ -176,41 +176,41 @@ export function getBaseArchitecture(name: string): string | null {
|
||||
export function getCompatibleArchitectures(arch: string): string[] {
|
||||
const normalized = normalizeArchitecture(arch)
|
||||
const parentMap = PARENT_MAP as Record<string, string | null>
|
||||
|
||||
|
||||
const compatible = [normalized]
|
||||
const visited = new Set<string>()
|
||||
let current = normalized
|
||||
|
||||
|
||||
// Follow parent chain up to base architecture
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current)
|
||||
const parent = parentMap[current]
|
||||
|
||||
|
||||
if (parent === null) {
|
||||
// Reached base architecture
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
if (parent === undefined) {
|
||||
// Unknown, stop here
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
const normalizedParent = normalizeArchitecture(parent)
|
||||
if (!compatible.includes(normalizedParent)) {
|
||||
compatible.push(normalizedParent)
|
||||
}
|
||||
|
||||
|
||||
current = normalizedParent
|
||||
}
|
||||
|
||||
|
||||
return compatible
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a plugin is compatible with a target
|
||||
* Plugin can specify includes/excludes arrays with targets, variant bases, or architectures
|
||||
*
|
||||
*
|
||||
* @param pluginIncludes - Array of architectures/targets the plugin explicitly supports
|
||||
* @param pluginExcludes - Array of architectures/targets the plugin explicitly doesn't support
|
||||
* @param targetName - The target name to check compatibility against
|
||||
@@ -226,40 +226,40 @@ export function isPluginCompatibleWithTarget(
|
||||
}
|
||||
|
||||
const parentMap = PARENT_MAP as Record<string, string | null>
|
||||
|
||||
|
||||
// Normalize target name first (all keys in parentMap are normalized)
|
||||
const normalizedTarget = normalizeArchitecture(targetName)
|
||||
|
||||
|
||||
// Get all compatible names for the target (target itself + all parents up to base architecture)
|
||||
const compatibleNames = new Set<string>([normalizedTarget])
|
||||
const visited = new Set<string>()
|
||||
let current = normalizedTarget
|
||||
|
||||
|
||||
// Follow parent chain (all keys and values in parentMap are already normalized)
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current)
|
||||
const parent = parentMap[current]
|
||||
|
||||
|
||||
if (parent === null) {
|
||||
// Reached base architecture
|
||||
compatibleNames.add(current) // Add the base architecture itself
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
if (parent === undefined) {
|
||||
// Unknown, stop here
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// Parent is already normalized (from JSON)
|
||||
compatibleNames.add(parent)
|
||||
current = parent
|
||||
}
|
||||
|
||||
|
||||
// Check excludes first - if target matches any exclude, it's incompatible
|
||||
// compatibleNames are already normalized, normalize excludes for comparison
|
||||
if (pluginExcludes && pluginExcludes.length > 0) {
|
||||
const isExcluded = pluginExcludes.some((exclude) => {
|
||||
const isExcluded = pluginExcludes.some(exclude => {
|
||||
const normalizedExclude = normalizeArchitecture(exclude)
|
||||
return compatibleNames.has(normalizedExclude)
|
||||
})
|
||||
@@ -267,16 +267,16 @@ export function isPluginCompatibleWithTarget(
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If includes are specified, target must match at least one include
|
||||
// compatibleNames are already normalized, normalize includes for comparison
|
||||
if (pluginIncludes && pluginIncludes.length > 0) {
|
||||
return pluginIncludes.some((include) => {
|
||||
return pluginIncludes.some(include => {
|
||||
const normalizedInclude = normalizeArchitecture(include)
|
||||
return compatibleNames.has(normalizedInclude)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// If no includes/excludes specified, assume compatible with all (backward compatible)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// https://vike.dev/Head
|
||||
|
||||
import logoUrl from "../assets/logo.png";
|
||||
import faviconUrl from "../assets/favicon.svg";
|
||||
import appleTouchIconUrl from "../assets/apple-touch-icon.png";
|
||||
import siteWebmanifestUrl from "../assets/site.webmanifest";
|
||||
import favicon96x96Url from "../assets/favicon-96x96.png";
|
||||
import faviconIcoUrl from "../assets/favicon.ico";
|
||||
import appleTouchIconUrl from "../assets/apple-touch-icon.png"
|
||||
import favicon96x96Url from "../assets/favicon-96x96.png"
|
||||
import faviconIcoUrl from "../assets/favicon.ico"
|
||||
import faviconUrl from "../assets/favicon.svg"
|
||||
import logoUrl from "../assets/logo.png"
|
||||
import siteWebmanifestUrl from "../assets/site.webmanifest"
|
||||
|
||||
export function Head() {
|
||||
return (
|
||||
@@ -18,5 +18,5 @@ export function Head() {
|
||||
<link rel="manifest" href={siteWebmanifestUrl} />
|
||||
<link rel="icon" href={logoUrl} />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import "./Layout.css";
|
||||
import "./tailwind.css";
|
||||
import logoUrl from "../assets/logo.png";
|
||||
import { Link } from "../components/Link";
|
||||
import { ConvexReactClient } from "convex/react";
|
||||
import { ConvexAuthProvider } from "@convex-dev/auth/react";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import { usePageContext } from "vike-react/usePageContext";
|
||||
import Navbar from "@/components/Navbar"
|
||||
import { ConvexAuthProvider } from "@convex-dev/auth/react"
|
||||
import { ConvexReactClient } from "convex/react"
|
||||
import { usePageContext } from "vike-react/usePageContext"
|
||||
import "./Layout.css"
|
||||
import "./tailwind.css"
|
||||
|
||||
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
||||
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string)
|
||||
|
||||
function ConditionalNavbar() {
|
||||
const pageContext = usePageContext();
|
||||
const pageContext = usePageContext()
|
||||
if (pageContext.urlPathname === "/") {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Navbar />;
|
||||
return <Navbar />
|
||||
}
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
@@ -23,5 +21,5 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
<ConditionalNavbar />
|
||||
{children}
|
||||
</ConvexAuthProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export async function onPageTransitionEnd() {
|
||||
console.log("Page transition end");
|
||||
document.body.classList.remove("page-transition");
|
||||
console.log("Page transition end")
|
||||
document.body.classList.remove("page-transition")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// https://vike.dev/onPageTransitionStart
|
||||
|
||||
import type { PageContextClient } from "vike/types";
|
||||
import type { PageContextClient } from "vike/types"
|
||||
|
||||
export async function onPageTransitionStart(pageContext: Partial<PageContextClient>) {
|
||||
console.log("Page transition start");
|
||||
console.log("pageContext.isBackwardNavigation", pageContext.isBackwardNavigation);
|
||||
document.body.classList.add("page-transition");
|
||||
console.log("Page transition start")
|
||||
console.log("pageContext.isBackwardNavigation", pageContext.isBackwardNavigation)
|
||||
document.body.classList.add("page-transition")
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { usePageContext } from "vike-react/usePageContext";
|
||||
import { usePageContext } from "vike-react/usePageContext"
|
||||
|
||||
export default function Page() {
|
||||
const { is404 } = usePageContext();
|
||||
const { is404 } = usePageContext()
|
||||
if (is404) {
|
||||
return (
|
||||
<>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>This page could not be found.</p>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h1>Internal Error</h1>
|
||||
<p>Something went wrong.</p>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useMutation, useQuery } from "convex/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { navigate } from "vike/client/router";
|
||||
import { BuildDownloadButton } from "@/components/BuildDownloadButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import type { Id } from "../../convex/_generated/dataModel";
|
||||
import { ArtifactType } from "../../convex/builds";
|
||||
import { BuildDownloadButton } from "@/components/BuildDownloadButton"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useMutation, useQuery } from "convex/react"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { navigate } from "vike/client/router"
|
||||
import { api } from "../../convex/_generated/api"
|
||||
import type { Id } from "../../convex/_generated/dataModel"
|
||||
import { ArtifactType } from "../../convex/builds"
|
||||
|
||||
type FilterType = "all" | "failed";
|
||||
type FilterType = "all" | "failed"
|
||||
|
||||
export default function Admin() {
|
||||
const [filter, setFilter] = useState<FilterType>("failed");
|
||||
const isAdmin = useQuery(api.admin.isAdmin);
|
||||
const failedBuilds = useQuery(api.admin.listFailedBuilds);
|
||||
const allBuilds = useQuery(api.admin.listAllBuilds);
|
||||
const retryBuild = useMutation(api.admin.retryBuild);
|
||||
const [filter, setFilter] = useState<FilterType>("failed")
|
||||
const isAdmin = useQuery(api.admin.isAdmin)
|
||||
const failedBuilds = useQuery(api.admin.listFailedBuilds)
|
||||
const allBuilds = useQuery(api.admin.listAllBuilds)
|
||||
const retryBuild = useMutation(api.admin.retryBuild)
|
||||
|
||||
const builds = filter === "failed" ? failedBuilds : allBuilds;
|
||||
const builds = filter === "failed" ? failedBuilds : allBuilds
|
||||
|
||||
// Show loading state
|
||||
if (isAdmin === undefined) {
|
||||
@@ -25,7 +25,7 @@ export default function Admin() {
|
||||
<div className="min-h-screen bg-slate-950 text-white flex items-center justify-center">
|
||||
<div className="text-slate-400">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Redirect if not admin
|
||||
@@ -38,25 +38,25 @@ export default function Admin() {
|
||||
<Button onClick={() => navigate("/")}>Go Home</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const handleRetry = async (buildId: Id<"builds">) => {
|
||||
try {
|
||||
await retryBuild({ buildId });
|
||||
await retryBuild({ buildId })
|
||||
toast.success("Build retry initiated", {
|
||||
description: "The build has been queued with the latest YAML.",
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
toast.error("Failed to retry build", {
|
||||
description: String(error),
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
return new Date(timestamp).toLocaleString()
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
@@ -71,14 +71,14 @@ export default function Admin() {
|
||||
text: "text-yellow-400",
|
||||
label: "Queued",
|
||||
},
|
||||
};
|
||||
}
|
||||
const config = statusConfig[status as keyof typeof statusConfig] || {
|
||||
bg: "bg-slate-500/20",
|
||||
text: "text-slate-400",
|
||||
label: status,
|
||||
};
|
||||
return <span className={`px-2 py-1 ${config.bg} ${config.text} rounded text-sm`}>{config.label}</span>;
|
||||
};
|
||||
}
|
||||
return <span className={`px-2 py-1 ${config.bg} ${config.text} rounded text-sm`}>{config.label}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-8">
|
||||
@@ -112,7 +112,7 @@ export default function Admin() {
|
||||
<div className="text-center text-slate-400 py-12">No {filter === "failed" ? "failed " : ""}builds found.</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{builds.map((build) => (
|
||||
{builds.map(build => (
|
||||
<div key={build._id} className="bg-slate-900 border border-slate-800 rounded-lg p-6">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-slate-800">
|
||||
@@ -191,7 +191,7 @@ export default function Admin() {
|
||||
{build.githubRunId}
|
||||
</a>
|
||||
)}
|
||||
{build.githubRunIdHistory?.map((id) => (
|
||||
{build.githubRunIdHistory?.map(id => (
|
||||
<a
|
||||
key={id}
|
||||
href={`https://github.com/MeshEnvy/mesh-forge/actions/runs/${id}`}
|
||||
@@ -219,5 +219,5 @@ export default function Admin() {
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Builder from "./Builder";
|
||||
import Builder from "./Builder"
|
||||
|
||||
export default function BuildNew() {
|
||||
return <Builder />;
|
||||
return <Builder />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Builder from "../Builder";
|
||||
import Builder from "../Builder"
|
||||
|
||||
export default function BuildNew() {
|
||||
return <Builder />;
|
||||
return <Builder />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from "../../components/Link";
|
||||
import { usePageContext } from "vike-react/usePageContext";
|
||||
import { usePageContext } from "vike-react/usePageContext"
|
||||
import { Link } from "../../components/Link"
|
||||
|
||||
const navSections = [
|
||||
{
|
||||
@@ -19,12 +19,12 @@ const navSections = [
|
||||
{ href: "/docs/nRF52", label: "nRF52" },
|
||||
],
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
function NavLink({ href, label }: { href: string; label: string }) {
|
||||
const pageContext = usePageContext();
|
||||
const { urlPathname } = pageContext;
|
||||
const isActive = href === "/docs" ? urlPathname === href : urlPathname.startsWith(href);
|
||||
const pageContext = usePageContext()
|
||||
const { urlPathname } = pageContext
|
||||
const isActive = href === "/docs" ? urlPathname === href : urlPathname.startsWith(href)
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
@@ -38,7 +38,7 @@ function NavLink({ href, label }: { href: string; label: string }) {
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
@@ -55,7 +55,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</h3>
|
||||
)}
|
||||
<ul className="space-y-1 mt-1">
|
||||
{section.items.map((item) => (
|
||||
{section.items.map(item => (
|
||||
<li key={item.href}>
|
||||
<NavLink href={item.href} label={item.label} />
|
||||
</li>
|
||||
@@ -70,5 +70,5 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
<article className="prose prose-invert lg:prose-xl max-w-none">{children}</article>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,4 +54,3 @@ If you want to create and contribute plugins to the registry, check out the [Plu
|
||||
|
||||
- [MeshEnvy](https://meshenvy.org) - Built by MeshEnvy (not affiliated with Meshtastic)
|
||||
- [Meshtastic](https://meshtastic.org) - Learn more about Meshtastic
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@ const config = {
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
};
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
||||
2
vendor/web-flasher
vendored
2
vendor/web-flasher
vendored
Submodule vendor/web-flasher updated: 9b96dd662d...b9668e534d
@@ -1,9 +1,9 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import vike from "vike/plugin";
|
||||
import { defineConfig } from "vite";
|
||||
import path from "node:path";
|
||||
import mdx from "@mdx-js/rollup";
|
||||
import mdx from "@mdx-js/rollup"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import path from "node:path"
|
||||
import vike from "vike/plugin"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vike(), mdx(), react(), tailwindcss()],
|
||||
@@ -12,4 +12,4 @@ export default defineConfig({
|
||||
"@": path.resolve(__dirname, "."),
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user