refactor: update build management to use artifactPath instead of artifactUrl, enhance build retrieval logic, and streamline profileBuild handling for improved efficiency

This commit is contained in:
Ben Allfree
2025-11-24 02:52:15 -08:00
parent ab47ce5464
commit e54449464c
10 changed files with 358 additions and 472 deletions

View File

@@ -8,7 +8,6 @@ import modulesData from './modules.json'
type BuildUpdateData = {
status: string
completedAt?: number
artifactUrl?: string
}
/**
@@ -38,27 +37,25 @@ async function computeBuildHash(
}
/**
* Constructs the R2 artifact URL from a build hash.
* Uses the R2 public URL pattern: https://<bucket>.<account-id>.r2.cloudflarestorage.com/<hash>.uf2
* Constructs the R2 artifact URL from a build.
* Uses artifactPath if available, otherwise falls back to buildHash.uf2
* Or custom domain if R2_PUBLIC_URL is set.
*/
function getR2ArtifactUrl(buildHash: string): string {
export function getR2ArtifactUrl(build: {
buildHash: string
artifactPath?: string
}): string {
const r2PublicUrl = process.env.R2_PUBLIC_URL
if (r2PublicUrl) {
// Custom domain configured
return `${r2PublicUrl}/${buildHash}.uf2`
if (!r2PublicUrl) {
throw new Error('R2_PUBLIC_URL is not set')
}
// Default R2 public URL pattern (requires public bucket)
const bucketName = process.env.R2_BUCKET_NAME || 'firmware-builds'
const accountId = process.env.R2_ACCOUNT_ID || ''
if (accountId) {
return `https://${bucketName}.${accountId}.r2.cloudflarestorage.com/${buildHash}.uf2`
}
// Fallback: assume custom domain or public bucket URL
return `https://${bucketName}.r2.cloudflarestorage.com/${buildHash}.uf2`
const path = build.artifactPath || `/${build.buildHash}.uf2`
// Ensure path starts with /
const normalizedPath = path.startsWith('/') ? path : `/${path}`
return `${r2PublicUrl}${normalizedPath}`
}
export const triggerFlash = mutation({
export const triggerBuildViaProfile = mutation({
args: {
profileId: v.id('profiles'),
target: v.string(),
@@ -90,7 +87,7 @@ export const triggerFlash = mutation({
)
// Check if build already exists with this hash
const existingBuild = await ctx.db
let existingBuild = await ctx.db
.query('builds')
.withIndex('by_hash', (q) => q.eq('buildHash', buildHash))
.first()
@@ -102,90 +99,54 @@ export const triggerFlash = mutation({
// Build already exists, use it
buildId = existingBuild._id
} else {
// Check cache for existing build
const cached = await ctx.db
.query('buildCache')
.withIndex('by_hash_target', (q) =>
q.eq('buildHash', buildHash).eq('target', args.target)
)
.first()
if (cached) {
// Use cached artifact, create build with success status
const artifactUrl = getR2ArtifactUrl(buildHash)
buildId = await ctx.db.insert('builds', {
target: args.target,
githubRunId: 0,
status: 'success',
artifactUrl: artifactUrl,
startedAt: Date.now(),
completedAt: Date.now(),
buildHash: buildHash,
})
} else {
// Not cached, create new build and dispatch workflow
buildId = await ctx.db.insert('builds', {
target: args.target,
githubRunId: 0,
status: 'queued',
startedAt: Date.now(),
buildHash: buildHash,
})
shouldDispatch = true
}
// Create new build and dispatch workflow
buildId = await ctx.db.insert('builds', {
target: args.target,
githubRunId: 0,
status: 'queued',
startedAt: Date.now(),
buildHash: buildHash,
})
shouldDispatch = true
// Handle race condition
const raceCheckBuild = await ctx.db
existingBuild = await ctx.db
.query('builds')
.withIndex('by_hash', (q) => q.eq('buildHash', buildHash))
.first()
if (raceCheckBuild && raceCheckBuild._id !== buildId) {
if (existingBuild && existingBuild._id !== buildId) {
await ctx.db.delete(buildId)
buildId = raceCheckBuild._id
buildId = existingBuild._id
shouldDispatch = false
}
}
// Create or get profileTarget
let profileTarget = await ctx.db
.query('profileTargets')
.withIndex('by_profile_target', (q) =>
q.eq('profileId', args.profileId).eq('target', args.target)
)
.first()
// Create or update profileBuild record
// Check if a profileBuild already exists for this profile+target combination
const profileBuilds = await ctx.db
.query('profileBuilds')
.withIndex('by_profile', (q) => q.eq('profileId', args.profileId))
.collect()
if (!profileTarget) {
const newProfileTargetId = await ctx.db.insert('profileTargets', {
profileId: args.profileId,
target: args.target,
createdAt: Date.now(),
})
const retrieved = await ctx.db.get(newProfileTargetId)
if (!retrieved) {
throw new Error('Failed to create profileTarget')
// Find existing profileBuild with matching target by checking the build
let foundExisting = null
for (const pb of profileBuilds) {
const build = await ctx.db.get(pb.buildId)
if (build?.target === args.target) {
foundExisting = pb
break
}
profileTarget = retrieved
}
// Create or update profileBuild record
const existingProfileBuild = await ctx.db
.query('profileBuilds')
.withIndex('by_profile_target', (q) =>
q.eq('profileId', args.profileId).eq('target', args.target)
)
.first()
if (existingProfileBuild) {
await ctx.db.patch(existingProfileBuild._id, {
if (foundExisting) {
await ctx.db.patch(foundExisting._id, {
buildId: buildId,
})
} else {
await ctx.db.insert('profileBuilds', {
profileId: args.profileId,
buildId: buildId,
target: args.target,
createdAt: Date.now(),
})
}
@@ -200,7 +161,7 @@ export const triggerFlash = mutation({
})
}
return profileTarget._id
return buildId
},
})
@@ -217,7 +178,12 @@ export const listByProfile = query({
const builds = await Promise.all(
profileBuilds.map(async (pb) => {
const build = await ctx.db.get(pb.buildId)
return build
if (!build) return null
// Return build with computed artifactUrl
return {
...build,
artifactUrl: getR2ArtifactUrl(build),
}
})
)
@@ -249,7 +215,11 @@ export const get = query({
for (const pb of profileBuilds) {
const profile = await ctx.db.get(pb.profileId)
if (profile && profile.userId === userId) {
return build
// Return build with computed artifactUrl
return {
...build,
artifactUrl: getR2ArtifactUrl(build),
}
}
}
@@ -318,13 +288,13 @@ export const updateBuildStatus = internalMutation({
args: {
buildId: v.id('builds'),
status: v.string(), // Accepts any status string value
artifactUrl: v.optional(v.string()),
artifactPath: v.optional(v.string()),
},
handler: async (ctx, args) => {
const build = await ctx.db.get(args.buildId)
if (!build) return
const updateData: BuildUpdateData = {
const updateData: BuildUpdateData & { artifactPath?: string } = {
status: args.status,
}
@@ -333,52 +303,11 @@ export const updateBuildStatus = internalMutation({
updateData.completedAt = Date.now()
}
if (args.artifactUrl) {
updateData.artifactUrl = args.artifactUrl
// Set artifactPath if provided
if (args.artifactPath !== undefined) {
updateData.artifactPath = args.artifactPath
}
await ctx.db.patch(args.buildId, updateData)
// If build succeeded, store in cache with R2 URL
if (args.status === 'success' && build.buildHash && build.target) {
// Get version from any profileBuild linked to this build
const profileBuilds = await ctx.db
.query('profileBuilds')
.withIndex('by_build', (q) => q.eq('buildId', args.buildId))
.collect()
if (profileBuilds.length > 0) {
const profileBuild = profileBuilds[0]
const profile = await ctx.db.get(profileBuild.profileId)
if (profile) {
// Construct R2 URL from hash
const artifactUrl = getR2ArtifactUrl(build.buildHash)
// Update build with R2 URL if not already set
if (!args.artifactUrl) {
await ctx.db.patch(args.buildId, { artifactUrl })
}
// Check if cache entry already exists
const existing = await ctx.db
.query('buildCache')
.withIndex('by_hash_target', (q) =>
q.eq('buildHash', build.buildHash).eq('target', build.target)
)
.first()
if (!existing) {
// Store in cache
await ctx.db.insert('buildCache', {
buildHash: build.buildHash,
target: build.target,
artifactUrl: artifactUrl,
version: profile.version,
createdAt: Date.now(),
})
}
}
}
}
},
})

View File

@@ -34,6 +34,7 @@ http.route({
await ctx.runMutation(internal.builds.updateBuildStatus, {
buildId: payload.build_id,
status: payload.status,
artifactPath: payload.artifactPath,
})
return new Response(null, { status: 200 })

View File

@@ -1,6 +1,7 @@
import { getAuthUserId } from '@convex-dev/auth/server'
import { v } from 'convex/values'
import { mutation, query } from './_generated/server'
import { getR2ArtifactUrl } from './builds'
export const list = query({
args: {},
@@ -45,36 +46,50 @@ export const get = query({
export const getTargets = query({
args: { profileId: v.id('profiles') },
handler: async (ctx, args) => {
const profileTargets = await ctx.db
.query('profileTargets')
const profileBuilds = await ctx.db
.query('profileBuilds')
.withIndex('by_profile', (q) => q.eq('profileId', args.profileId))
.collect()
return profileTargets.map((pt) => pt.target)
// Get unique targets from builds
const builds = await Promise.all(
profileBuilds.map((pb) => ctx.db.get(pb.buildId))
)
const targets = new Set(
builds
.filter((b): b is NonNullable<typeof b> => b !== null)
.map((b) => b.target)
)
return Array.from(targets)
},
})
export const getProfileTarget = query({
args: { profileTargetId: v.id('profileTargets') },
args: {
profileId: v.id('profiles'),
target: v.string(),
},
handler: async (ctx, args) => {
const profileTarget = await ctx.db.get(args.profileTargetId)
if (!profileTarget) return null
// Get the associated build via profileBuilds
const profileBuild = await ctx.db
// Get all profileBuilds for this profile
const profileBuilds = await ctx.db
.query('profileBuilds')
.withIndex('by_profile_target', (q) =>
q
.eq('profileId', profileTarget.profileId)
.eq('target', profileTarget.target)
)
.first()
.withIndex('by_profile', (q) => q.eq('profileId', args.profileId))
.collect()
const build = profileBuild ? await ctx.db.get(profileBuild.buildId) : null
return {
profileTarget,
build,
// Find the profileBuild with matching target by checking the build
for (const profileBuild of profileBuilds) {
const build = await ctx.db.get(profileBuild.buildId)
if (build?.target === args.target) {
return {
profileBuild,
build: {
...build,
artifactUrl: getR2ArtifactUrl(build),
},
}
}
}
return null
},
})
@@ -118,16 +133,8 @@ export const create = mutation({
isPublic: args.isPublic ?? true,
})
// Create profileTargets entries
if (args.targets) {
for (const target of args.targets) {
await ctx.db.insert('profileTargets', {
profileId,
target,
createdAt: Date.now(),
})
}
}
// Note: targets are now tracked via profileBuilds when builds are triggered
// No need to create profileTargets entries
return profileId
},
@@ -160,37 +167,8 @@ export const update = mutation({
updatedAt: Date.now(),
})
// Sync profileTargets if targets are provided
if (args.targets !== undefined) {
const newTargets = new Set(args.targets)
const existingProfileTargets = await ctx.db
.query('profileTargets')
.withIndex('by_profile', (q) => q.eq('profileId', args.id))
.collect()
const existingTargets = new Set(
existingProfileTargets.map((pt) => pt.target)
)
// Delete targets that are no longer in the list
for (const profileTarget of existingProfileTargets) {
if (!newTargets.has(profileTarget.target)) {
await ctx.db.delete(profileTarget._id)
}
}
// Add new targets
for (const target of args.targets) {
if (!existingTargets.has(target)) {
await ctx.db.insert('profileTargets', {
profileId: args.id,
target,
createdAt: Date.now(),
})
}
}
}
// Note: targets are now tracked via profileBuilds when builds are triggered
// No need to sync profileTargets
},
})
@@ -205,14 +183,14 @@ export const remove = mutation({
throw new Error('Unauthorized')
}
// Delete associated profileTargets
const profileTargets = await ctx.db
.query('profileTargets')
// Delete associated profileBuilds
const profileBuilds = await ctx.db
.query('profileBuilds')
.withIndex('by_profile', (q) => q.eq('profileId', args.id))
.collect()
for (const profileTarget of profileTargets) {
await ctx.db.delete(profileTarget._id)
for (const profileBuild of profileBuilds) {
await ctx.db.delete(profileBuild._id)
}
await ctx.db.delete(args.id)

View File

@@ -14,39 +14,21 @@ export default defineSchema({
})
.index('by_user', ['userId'])
.index('by_public', ['isPublic']),
builds: defineTable({
target: v.string(),
githubRunId: v.number(),
status: v.string(), // Accepts arbitrary status strings (e.g., "queued", "checking_out", "building", "uploading", "success", "failure")
artifactUrl: v.optional(v.string()),
startedAt: v.number(),
completedAt: v.optional(v.number()),
buildHash: v.string(),
artifactPath: v.optional(v.string()), // Path to artifact in R2 (e.g., "/abc123.uf2" or "/abc123.bin")
}).index('by_hash', ['buildHash']),
profileTargets: defineTable({
profileId: v.id('profiles'),
target: v.string(),
createdAt: v.number(),
})
.index('by_profile', ['profileId'])
.index('by_profile_target', ['profileId', 'target']),
profileBuilds: defineTable({
profileId: v.id('profiles'),
buildId: v.id('builds'),
target: v.string(),
createdAt: v.number(),
})
.index('by_profile', ['profileId'])
.index('by_build', ['buildId'])
.index('by_profile_target', ['profileId', 'target']),
buildCache: defineTable({
buildHash: v.string(),
target: v.string(),
artifactUrl: v.string(),
version: v.string(),
createdAt: v.number(),
}).index('by_hash_target', ['buildHash', 'target']),
.index('by_build', ['buildId']),
})

View File

@@ -24,7 +24,7 @@ function App() {
<Route path="/" element={<LandingPage />} />
<Route path="/profiles/:id" element={<ProfileDetail />} />
<Route
path="/profiles/:id/flash/:profileTargetId"
path="/profiles/:id/flash/:target"
element={<ProfileFlash />}
/>
<Route path="*" element={<Navigate to="/" replace />} />
@@ -39,7 +39,7 @@ function App() {
<Route path="/builds/:buildId" element={<BuildDetail />} />
<Route path="/profiles/:id" element={<ProfileDetail />} />
<Route
path="/profiles/:id/flash/:profileTargetId"
path="/profiles/:id/flash/:target"
element={<ProfileFlash />}
/>
<Route path="*" element={<Navigate to="/" replace />} />

View File

@@ -23,4 +23,3 @@ export default function ProfileTargets({ profileId }: ProfileTargetsProps) {
</span>
)
}

View File

@@ -1,242 +1,242 @@
// This file is auto-generated by scripts/generate-versions.js
export const VERSIONS = [
'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
"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]
export type FirmwareVersion = typeof VERSIONS[number];

View File

@@ -13,9 +13,9 @@ export default function Dashboard() {
const profiles = useQuery(api.profiles.list)
const removeProfile = useMutation(api.profiles.remove)
const [isEditing, setIsEditing] = useState(false)
const [editingProfile, setEditingProfile] = useState<Doc<'profiles'> | null>(
null
)
const [editingProfile, setEditingProfile] = useState<
Doc<'profiles'> | undefined
>(undefined)
const handleEdit = (profile: Doc<'profiles'>) => {
setEditingProfile(profile)
@@ -23,7 +23,7 @@ export default function Dashboard() {
}
const handleCreate = () => {
setEditingProfile(null)
setEditingProfile(undefined)
setIsEditing(true)
}

View File

@@ -12,7 +12,7 @@ import { TARGETS } from '../constants/targets'
export default function ProfileDetail() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const triggerFlash = useMutation(api.builds.triggerFlash)
const triggerBuildViaProfile = useMutation(api.builds.triggerBuildViaProfile)
const profile = useQuery(
api.profiles.get,
id ? { id: id as Id<'profiles'> } : 'skip'
@@ -81,11 +81,11 @@ export default function ProfileDetail() {
if (!selectedTarget || !id) return
try {
const profileTargetId = await triggerFlash({
await triggerBuildViaProfile({
profileId: id as Id<'profiles'>,
target: selectedTarget,
})
navigate(`/profiles/${id}/flash/${profileTargetId}`)
navigate(`/profiles/${id}/flash/${selectedTarget}`)
} catch (error) {
toast.error('Failed to start flash', {
description: String(error),

View File

@@ -13,16 +13,14 @@ import { api } from '../../convex/_generated/api'
import type { Id } from '../../convex/_generated/dataModel'
export default function ProfileFlash() {
const { id, profileTargetId } = useParams<{
const { id, target } = useParams<{
id: string
profileTargetId: string
target: string
}>()
const data = useQuery(
api.profiles.getProfileTarget,
profileTargetId
? { profileTargetId: profileTargetId as Id<'profileTargets'> }
: 'skip'
id && target ? { profileId: id as Id<'profiles'>, target } : 'skip'
)
if (data === undefined) {
@@ -52,7 +50,6 @@ export default function ProfileFlash() {
}
const build = data.build
const profileTarget = data.profileTarget
const getStatusColor = (status: string) => {
if (status === 'success') return 'text-green-400'
@@ -90,7 +87,7 @@ export default function ProfileFlash() {
<div className="flex items-center gap-4">
{getStatusIcon(build.status)}
<div>
<h1 className="text-3xl font-bold">{profileTarget.target}</h1>
<h1 className="text-3xl font-bold">{target}</h1>
<div className="flex items-center gap-2 text-slate-400 mt-1">
<span className={getStatusColor(build.status)}>
{humanizeStatus(build.status)}