feat(wizard): gate stage navigation on disk artefacts and remove test_target fallbacks (v0.1.15) #75

Merged
gronod merged 1 commits from fix/issue-60-wizard-disk-artefact-gating into development 2026-08-25 10:47:20 +01:00
13 changed files with 170 additions and 33 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "iccery",
"private": true,
"version": "0.1.14",
"version": "0.1.15",
"type": "module",
"scripts": {
"tauri": "tauri"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "iccery"
version = "0.1.14"
version = "0.1.15"
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"]
edition = "2021"
+37
View File
@@ -122,6 +122,43 @@ pub fn resolve_profile_extension(cwd: &str, basename: &str) -> (String, std::pat
}
}
#[derive(Serialize)]
pub struct StageStatus {
pub stage1_complete: bool,
pub stage2_complete: bool,
pub stage3_complete: bool,
pub stage4_complete: bool,
pub profile_path: Option<String>,
}
#[tauri::command]
pub fn verify_stage_artefacts(cwd: String, basename: String) -> StageStatus {
if cwd.trim().is_empty() || basename.trim().is_empty() {
return StageStatus {
stage1_complete: false,
stage2_complete: false,
stage3_complete: false,
stage4_complete: false,
profile_path: None,
};
}
let base_path = std::path::Path::new(&cwd);
let ti1 = base_path.join(format!("{}.ti1", basename)).exists();
let ti2 = base_path.join(format!("{}.ti2", basename)).exists();
let ti3 = base_path.join(format!("{}.ti3", basename)).exists();
let (_, prof_p) = resolve_profile_extension(&cwd, &basename);
let prof_exists = prof_p.exists();
StageStatus {
stage1_complete: ti1,
stage2_complete: ti2,
stage3_complete: ti3,
stage4_complete: prof_exists,
profile_path: if prof_exists { Some(prof_p.to_string_lossy().to_string()) } else { None },
}
}
#[tauri::command]
pub fn get_profile_path(cwd: String, basename: String) -> String {
let (_, path) = resolve_profile_extension(&cwd, &basename);
+1
View File
@@ -19,6 +19,7 @@ pub fn run() {
commands::resolve_binary,
commands::detect_instruments,
commands::get_profile_path,
commands::verify_stage_artefacts,
commands::extract_gamut,
commands::run_targen,
commands::run_printtarg,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery",
"version": "0.1.14",
"version": "0.1.15",
"identifier": "com.gronod.iccery",
"build": {
"frontendDist": "../src"
+8
View File
@@ -5,6 +5,7 @@ import { initColprof } from './colprof.js';
import { initProfcheck } from './profcheck.js';
import { initSettings } from './settings.js';
import { initGamutViewer } from './gamut_viewer.js';
import { wizardState } from './state.js';
const { invoke } = window.__TAURI__.core;
@@ -13,8 +14,15 @@ document.addEventListener('DOMContentLoaded', () => {
const steps = document.querySelectorAll('.step');
const stages = document.querySelectorAll('.stage');
// Initialize gating on load
wizardState.updateGating();
steps.forEach(step => {
step.addEventListener('click', () => {
if (step.classList.contains('disabled')) {
return;
}
const targetStep = step.getAttribute('data-step');
// Update UI
+16 -7
View File
@@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
import { startSwatchListener, stopSwatchListener } from './swatch_grid.js';
import { setStage3Result } from './colprof.js';
import { wizardState } from './state.js';
// Module-level state: set by Stage 2 when it completes
let stage2Basename = "";
@@ -11,8 +12,9 @@ let stage2Cwd = "";
* Called by printtarg.js after Stage 2 completes.
*/
export function setStage2Result(basename, cwd) {
stage2Basename = basename;
stage2Cwd = cwd;
stage2Basename = basename || wizardState.basename;
stage2Cwd = cwd || wizardState.cwd;
wizardState.setTarget(stage2Basename, stage2Cwd);
}
// State machine states
@@ -84,11 +86,17 @@ export function initChartread() {
// Start reading button
if (btnStartRead) {
btnStartRead.addEventListener("click", async () => {
if (!stage2Basename) {
const basename = stage2Basename || wizardState.basename;
const cwd = stage2Cwd || wizardState.cwd;
if (!basename || !cwd) {
setPrompt("Error: No .ti2 file available. Complete Stage 2 first.");
return;
}
stage2Basename = basename;
stage2Cwd = cwd;
logPre.textContent = "";
logContainer.open = false;
logContainer.classList.remove("hidden");
@@ -96,11 +104,11 @@ export function initChartread() {
setPrompt("Starting chartread... waiting for instrument calibration prompt.");
const config = {
basename: stage2Basename,
cwd: stage2Cwd,
basename: basename,
cwd: cwd,
};
currentProcessId = `chartread_${stage2Basename}`;
currentProcessId = `chartread_${basename}`;
// Start swatch grid listener
await startSwatchListener(currentProcessId);
@@ -152,7 +160,8 @@ export function initChartread() {
setState(STATE.FINISHED);
setPrompt("✅ Measurement complete! .ti3 file has been saved.");
logPre.textContent += "\n[SUCCESS] chartread completed. .ti3 file written.\n";
setStage3Result(stage2Basename, stage2Cwd);
wizardState.setTarget(basename, cwd);
setStage3Result(basename, cwd);
advanceToStage4();
} else {
setState(STATE.FINISHED);
+18 -4
View File
@@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
import { setStage4Result } from './profcheck.js';
import { loadGamutMesh } from './gamut_viewer.js';
import { wizardState } from './state.js';
let chartreadBasename = "";
let chartreadCwd = "";
@@ -10,8 +11,9 @@ let chartreadCwd = "";
* Called by chartread.js after Stage 3 completes.
*/
export function setStage3Result(basename, cwd) {
chartreadBasename = basename;
chartreadCwd = cwd;
chartreadBasename = basename || wizardState.basename;
chartreadCwd = cwd || wizardState.cwd;
wizardState.setTarget(chartreadBasename, chartreadCwd);
}
export function initColprof() {
@@ -31,8 +33,19 @@ export function initColprof() {
if (!btnCreateProfile) return;
btnCreateProfile.addEventListener("click", async () => {
const basename = chartreadBasename || "test_target";
const cwd = chartreadCwd || "";
const basename = chartreadBasename || wizardState.basename;
const cwd = chartreadCwd || wizardState.cwd;
if (!basename || !cwd) {
logPre.textContent = "[ERROR] No .ti3 measurement file available. Please complete Stage 3 first.\n";
logContainer.open = true;
logContainer.classList.remove("hidden");
btnCreateProfile.disabled = false;
return;
}
chartreadBasename = basename;
chartreadCwd = cwd;
const description = descInput.value.trim() || basename;
@@ -104,6 +117,7 @@ export function initColprof() {
successInfo.textContent = `Profile: ${displayFilename} (${description})`;
successCard.classList.remove("hidden");
wizardState.setTarget(basename, cwd);
setStage4Result(basename, cwd);
// Automatically extract gamut mesh for 3D visualization
+5 -2
View File
@@ -1,6 +1,7 @@
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
import { setStage2Result } from './chartread.js';
import { wizardState } from './state.js';
// Module-level state: set by Stage 1 when it completes
let stage1Basename = "";
@@ -13,8 +14,9 @@ let discoveredPrinters = [];
* Passes the basename and working directory forward.
*/
export function setStage1Result(basename, cwd) {
stage1Basename = basename;
stage1Cwd = cwd;
stage1Basename = basename || wizardState.basename;
stage1Cwd = cwd || wizardState.cwd;
wizardState.setTarget(stage1Basename, stage1Cwd);
}
export function initPrinttarg() {
@@ -347,6 +349,7 @@ export function initPrinttarg() {
showNotification("info", "Target pages generated. Select your destination printer below and print with color management strictly bypassed.");
}
wizardState.setTarget(stage1Basename, stage1Cwd);
setStage2Result(stage1Basename, stage1Cwd);
} else {
logPre.textContent += `\n[ERROR] printtarg exited with code ${event.payload.code}.\n`;
+17 -4
View File
@@ -1,6 +1,7 @@
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
import { loadGamutMesh } from './gamut_viewer.js';
import { wizardState } from './state.js';
let profileBasename = "";
let profileCwd = "";
@@ -9,8 +10,9 @@ let profileCwd = "";
* Called by colprof.js after Stage 4 completes.
*/
export function setStage4Result(basename, cwd) {
profileBasename = basename;
profileCwd = cwd;
profileBasename = basename || wizardState.basename;
profileCwd = cwd || wizardState.cwd;
wizardState.setTarget(profileBasename, profileCwd);
}
export function initProfcheck() {
@@ -26,8 +28,19 @@ export function initProfcheck() {
if (!btnVerify) return;
btnVerify.addEventListener("click", async () => {
const basename = profileBasename || "test_target";
const cwd = profileCwd || "";
const basename = profileBasename || wizardState.basename;
const cwd = profileCwd || wizardState.cwd;
if (!basename || !cwd) {
logPre.textContent = "[ERROR] No profile available to verify. Please complete Stage 4 first.\n";
logContainer.open = true;
logContainer.classList.remove("hidden");
btnVerify.disabled = false;
return;
}
profileBasename = basename;
profileCwd = cwd;
const sep = cwd.includes('\\') ? '\\' : '/';
const ti3Path = cwd ? `${cwd}${sep}${basename}.ti3` : `${basename}.ti3`;
+54 -11
View File
@@ -1,16 +1,59 @@
// Simple state machine for the UI
export const state = {
const { invoke } = window.__TAURI__.core;
export const wizardState = {
currentStage: 1,
profileName: 'default_profile',
instrument: null,
basename: "",
cwd: "",
update(newState) {
Object.assign(this, newState);
this.render();
setTarget(basename, cwd) {
if (basename) this.basename = basename;
if (cwd) this.cwd = cwd;
this.updateGating();
},
render() {
// Reactive UI updates go here
console.log('State updated:', this);
async updateGating() {
const steps = document.querySelectorAll('.step');
if (!steps || steps.length === 0) return;
if (!this.basename || !this.cwd) {
steps.forEach((step, idx) => {
if (idx === 0) {
step.classList.remove('disabled');
} else {
step.classList.add('disabled');
}
});
return;
}
try {
const status = await invoke('verify_stage_artefacts', {
cwd: this.cwd,
basename: this.basename,
});
// Step 1: always accessible
// Step 2: unlocked if .ti1 exists
// Step 3: unlocked if .ti2 exists
// Step 4: unlocked if .ti3 exists
// Step 5: unlocked if profile exists
const unlocked = [
true,
status.stage1_complete,
status.stage2_complete,
status.stage3_complete,
status.stage4_complete,
];
steps.forEach((step, idx) => {
if (unlocked[idx]) {
step.classList.remove('disabled');
} else {
step.classList.add('disabled');
}
});
} catch (e) {
console.warn("Could not verify stage artefacts:", e);
}
}
};
+2 -2
View File
@@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
const { save } = window.__TAURI__.dialog;
import { setStage1Result } from './printtarg.js';
import { wizardState } from './state.js';
export function initTargen() {
const colourSpaceRadios = document.querySelectorAll('input[name="colourSpace"]');
@@ -157,9 +158,8 @@ export function initTargen() {
if (event.payload.code === 0) {
logPre.textContent += "\n[SUCCESS] Targen completed successfully.\n";
// In a real app we'd dispatch an event to advance the stepper here.
// For now, we'll manually unlock stage 2 in the state.
btnGenerate.disabled = false;
wizardState.setTarget(basename, currentWorkingDir);
setStage1Result(basename, currentWorkingDir);
advanceToStage2();
} else {
+9
View File
@@ -93,6 +93,15 @@ body {
border-left: 4px solid var(--accent-color);
}
.step.disabled {
opacity: 0.3;
cursor: not-allowed;
}
.step.disabled:hover {
background-color: transparent;
}
.content {
flex: 1;
padding: 24px 32px;