Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions frontend/blok.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-08-24T23:08:33.167Z",
"generatedAt": "2026-08-28T09:29:26.782Z",
"app": {
"description": "",
"name": "newswitch",
Expand Down Expand Up @@ -4764,7 +4764,7 @@
],
"choices": null,
"default": null,
"description": "Configuration for a single optical filter.\n\nAttributes:\n slot: Filter wheel slot number.\n name: Human-readable filter name.\n center_wavelength: Center wavelength of the filter passband (nm).\n bandwidth: Full width at half maximum of the passband (nm).\n transmission: Peak transmission efficiency (0.0 to 1.0).\n is_active: Whether this filter is currently in the light path.\n",
"description": "Configuration for a single optical filter.\n\n Attributes:\n slot: Filter wheel slot number.\n name: Human-readable filter name.\n center_wavelength: Center wavelength of the filter passband (nm).\n bandwidth: Full width at half maximum of the passband (nm).\n transmission: Peak transmission efficiency (0.0 to 1.0).\n is_active: Whether this filter is currently in the light path.\n ",
"dimension": null,
"effects": [],
"identifier": "filter",
Expand Down Expand Up @@ -5086,7 +5086,7 @@
],
"choices": null,
"default": null,
"description": "Shared state for illumination parameters.\n\nNote: intensity is divided by 1000 in the detector for actual scaling,\nso 10000.0 gives an effective intensity of 10.0.\n",
"description": "Shared state for illumination parameters.\n\n Note: intensity is divided by 1000 in the detector for actual scaling,\n so 10000.0 gives an effective intensity of 10.0.\n ",
"dimension": null,
"effects": [],
"identifier": "illumination",
Expand Down Expand Up @@ -14264,12 +14264,12 @@
"stage_position"
],
"manipulates": [
"StageState",
"ObjectiveState",
"ExpanseState",
"CameraState",
"ExpanseState",
"FilterBankState",
"IlluminationState",
"FilterBankState"
"ObjectiveState",
"StageState"
],
"needsToken": true,
"optimistics": [],
Expand Down
59 changes: 44 additions & 15 deletions frontend/plugins/generate-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,33 @@ const sortKeysDeep = (value: unknown): unknown => {
return value;
};

// Rekuest derives these fields through Python sets, so their wire order changes with
// Python's per-process hash seed. Their order has no meaning; normalize it before the
// schema snapshot reaches blok.json so a fresh CI process cannot create false drift.
const sortSetLikeSchemaArrays = (value: unknown): unknown => {
if (Array.isArray(value)) {
return value.map(sortSetLikeSchemaArrays);
}

if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, child]) => {
const normalized = sortSetLikeSchemaArrays(child);
if (
(key === 'locks' || key === 'manipulates') &&
Array.isArray(normalized) &&
normalized.every((entry) => typeof entry === 'string')
) {
return [key, [...normalized].sort(byCodepoint)];
}
return [key, normalized];
}),
);
}

return value;
};

// Every write goes through here. Rewriting a byte-identical file would churn its mtime,
// which re-triggers Vite's watcher and makes `git status` dirty after a no-op codegen.
const writeIfChanged = (filePath: string, content: string) => {
Expand Down Expand Up @@ -1614,22 +1641,24 @@ export type AppDefinition = AppsDefinition[AppKey];

// Everything except generatedAt. Key order comes from the backend's JSON and is not
// stable, so deep-sort it before comparing or writing.
const blokPayload = sortKeysDeep({
app: {
name: (packageData.name as string | undefined) ?? 'unknown',
version: (packageData.version as string | undefined) ?? '0.0.0',
description: (packageData.description as string | undefined) ?? '',
startPage:
(packageData.homepage as string | undefined) ?? '/index.html',
type: (packageData.type as string | undefined) ?? 'unknown',
scripts: {
dev: scripts.dev ?? '',
build: scripts.build ?? '',
preview: scripts.preview ?? '',
const blokPayload = sortKeysDeep(
sortSetLikeSchemaArrays({
app: {
name: (packageData.name as string | undefined) ?? 'unknown',
version: (packageData.version as string | undefined) ?? '0.0.0',
description: (packageData.description as string | undefined) ?? '',
startPage:
(packageData.homepage as string | undefined) ?? '/index.html',
type: (packageData.type as string | undefined) ?? 'unknown',
scripts: {
dev: scripts.dev ?? '',
build: scripts.build ?? '',
preview: scripts.preview ?? '',
},
},
},
apps: blokApps,
}) as Record<string, unknown>;
apps: blokApps,
}),
) as Record<string, unknown>;

// Only stamp a new generatedAt when the content actually changed. Otherwise every
// dev-server start would dirty blok.json (780KB) for no reason.
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/apps/default/hooks/states/FilterBankState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const FilterSchema = z
})
.brand('filter')
.describe(
'Configuration for a single optical filter.\n\nAttributes:\n slot: Filter wheel slot number.\n name: Human-readable filter name.\n center_wavelength: Center wavelength of the filter passband (nm).\n bandwidth: Full width at half maximum of the passband (nm).\n transmission: Peak transmission efficiency (0.0 to 1.0).\n is_active: Whether this filter is currently in the light path.\n',
'Configuration for a single optical filter.\n\n Attributes:\n slot: Filter wheel slot number.\n name: Human-readable filter name.\n center_wavelength: Center wavelength of the filter passband (nm).\n bandwidth: Full width at half maximum of the passband (nm).\n transmission: Peak transmission efficiency (0.0 to 1.0).\n is_active: Whether this filter is currently in the light path.\n ',
);
export type Filter = z.input<typeof FilterSchema>;
export type FilterOutput = z.infer<typeof FilterSchema>;
Expand All @@ -23,7 +23,7 @@ export type FilterOutput = z.infer<typeof FilterSchema>;
export const FilterBankStateSchema = z.object({
filters: z.array(
FilterSchema.describe(
'Configuration for a single optical filter.\n\nAttributes:\n slot: Filter wheel slot number.\n name: Human-readable filter name.\n center_wavelength: Center wavelength of the filter passband (nm).\n bandwidth: Full width at half maximum of the passband (nm).\n transmission: Peak transmission efficiency (0.0 to 1.0).\n is_active: Whether this filter is currently in the light path.\n',
'Configuration for a single optical filter.\n\n Attributes:\n slot: Filter wheel slot number.\n name: Human-readable filter name.\n center_wavelength: Center wavelength of the filter passband (nm).\n bandwidth: Full width at half maximum of the passband (nm).\n transmission: Peak transmission efficiency (0.0 to 1.0).\n is_active: Whether this filter is currently in the light path.\n ',
),
),
current_slot: z.number(),
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/apps/default/hooks/states/IlluminationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const IlluminationSchema = z
})
.brand('illumination')
.describe(
'Shared state for illumination parameters.\n\nNote: intensity is divided by 1000 in the detector for actual scaling,\nso 10000.0 gives an effective intensity of 10.0.\n',
'Shared state for illumination parameters.\n\n Note: intensity is divided by 1000 in the detector for actual scaling,\n so 10000.0 gives an effective intensity of 10.0.\n ',
);
export type Illumination = z.input<typeof IlluminationSchema>;
export type IlluminationOutput = z.infer<typeof IlluminationSchema>;
Expand All @@ -26,7 +26,7 @@ export type IlluminationOutput = z.infer<typeof IlluminationSchema>;
export const IlluminationStateSchema = z.object({
illuminations: z.array(
IlluminationSchema.describe(
'Shared state for illumination parameters.\n\nNote: intensity is divided by 1000 in the detector for actual scaling,\nso 10000.0 gives an effective intensity of 10.0.\n',
'Shared state for illumination parameters.\n\n Note: intensity is divided by 1000 in the detector for actual scaling,\n so 10000.0 gives an effective intensity of 10.0.\n ',
),
),
});
Expand Down
Loading