mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-03-28 17:42:55 +01:00
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:
187
convex/builds.ts
187
convex/builds.ts
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']),
|
||||
})
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -23,4 +23,3 @@ export default function ProfileTargets({ profileId }: ProfileTargetsProps) {
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user