Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9102c95468 | ||
|
|
11e796fd17 | ||
|
|
a8c4025c01 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
|
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
|
||||||
|
|
||||||
[](https://git.i3omb.com/gronod/ICCery)
|
[](https://git.i3omb.com/gronod/ICCery)
|
||||||
[](https://git.i3omb.com/gronod/ICCery)
|
[](https://git.i3omb.com/gronod/ICCery)
|
||||||
[](https://tauri.app)
|
[](https://tauri.app)
|
||||||
[](LICENCE.md)
|
[](LICENCE.md)
|
||||||
|
|||||||
+1
-5
@@ -14,7 +14,7 @@ ICCery is a native, cross-platform desktop application built with:
|
|||||||
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
|
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
|
||||||
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
|
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
|
||||||
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
|
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
|
||||||
- **Current Version**: `v0.8.4` (Production release).
|
- **Current Version**: `v0.8.3` (Production release).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -113,10 +113,6 @@ ICCery is a native, cross-platform desktop application built with:
|
|||||||
- [x] **Custom Spectrum File Picker Dialog (#210)**: Implemented native `select_spectrum_file` command wrapping Tauri file dialog with `.sp` filter for custom FWA/OBA spectrum selection in Stage 4 profile generation.
|
- [x] **Custom Spectrum File Picker Dialog (#210)**: Implemented native `select_spectrum_file` command wrapping Tauri file dialog with `.sp` filter for custom FWA/OBA spectrum selection in Stage 4 profile generation.
|
||||||
- [x] **CGATS Dataset Import File Picker & State Synchronization (#211)**: Implemented native `select_dataset_file` command with `.ti3`, `.txt`, `.cgats`, and `.csv` filter, synchronized wizard target directory and basename upon import, and guarded against empty target states.
|
- [x] **CGATS Dataset Import File Picker & State Synchronization (#211)**: Implemented native `select_dataset_file` command with `.ti3`, `.txt`, `.cgats`, and `.csv` filter, synchronized wizard target directory and basename upon import, and guarded against empty target states.
|
||||||
|
|
||||||
### Maintenance & Reliability Release (`v0.8.4`)
|
|
||||||
- [x] **Atomic Verification History Persistence (#213)**: Hardened `quality_store.rs` with atomic temporary file writes (`.tmp`), explicit flush/sync, and atomic rename to prevent historical drift data loss or corruption upon unexpected system crashes.
|
|
||||||
- [x] **Frontend Unit Testing & CI Integration (#215)**: Added standard `npm test` script executing the 3 frontend test suites (`profcheck`, `chartread`, and `gamut_viewer`) and integrated automated frontend test validation into macOS, Linux, and Windows CI workflows.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Future Roadmap
|
## 3. Future Roadmap
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.4",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1423,7 +1423,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iccery"
|
name = "iccery"
|
||||||
version = "0.8.4"
|
version = "0.8.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iccery"
|
name = "iccery"
|
||||||
version = "0.8.4"
|
version = "0.8.3"
|
||||||
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
||||||
authors = ["Gordon"]
|
authors = ["Gordon"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -100,21 +100,15 @@ pub fn load_history_file(path: &Path) -> Vec<VerificationRecord> {
|
|||||||
}
|
}
|
||||||
let content = match fs::read_to_string(path) {
|
let content = match fs::read_to_string(path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(_) => return Vec::new(),
|
||||||
log::warn!("Failed to read verification history at {}: {}", path.display(), e);
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
match serde_json::from_str::<VerificationHistoryStore>(&content) {
|
match serde_json::from_str::<VerificationHistoryStore>(&content) {
|
||||||
Ok(store) => store.records,
|
Ok(store) => store.records,
|
||||||
Err(e) => {
|
Err(_) => Vec::new(),
|
||||||
log::error!("Failed to parse verification history at {}: {}", path.display(), e);
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes verification records to the specified JSON path atomically.
|
/// Writes verification records to the specified JSON path.
|
||||||
/// Automatically creates parent directories if needed.
|
/// Automatically creates parent directories if needed.
|
||||||
/// Evicts oldest records by timestamp if count exceeds HISTORY_CAP (1,000).
|
/// Evicts oldest records by timestamp if count exceeds HISTORY_CAP (1,000).
|
||||||
pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result<(), String> {
|
pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result<(), String> {
|
||||||
@@ -138,33 +132,7 @@ pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result
|
|||||||
|
|
||||||
let json = serde_json::to_string_pretty(&store)
|
let json = serde_json::to_string_pretty(&store)
|
||||||
.map_err(|e| format!("Failed to serialize verification history: {}", e))?;
|
.map_err(|e| format!("Failed to serialize verification history: {}", e))?;
|
||||||
|
fs::write(path, json).map_err(|e| format!("Failed to write verification history: {}", e))?;
|
||||||
let mut tmp_path = path.as_os_str().to_os_string();
|
|
||||||
tmp_path.push(".tmp");
|
|
||||||
let tmp_path = PathBuf::from(tmp_path);
|
|
||||||
|
|
||||||
// Write to temporary file with explicit flush and sync
|
|
||||||
{
|
|
||||||
use std::io::Write;
|
|
||||||
let mut file = fs::File::create(&tmp_path)
|
|
||||||
.map_err(|e| format!("Failed to create temp history file: {}", e))?;
|
|
||||||
file.write_all(json.as_bytes())
|
|
||||||
.map_err(|e| {
|
|
||||||
let _ = fs::remove_file(&tmp_path);
|
|
||||||
format!("Failed to write temp history file: {}", e)
|
|
||||||
})?;
|
|
||||||
file.sync_all()
|
|
||||||
.map_err(|e| {
|
|
||||||
let _ = fs::remove_file(&tmp_path);
|
|
||||||
format!("Failed to sync temp history file: {}", e)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomically replace destination file
|
|
||||||
if let Err(e) = fs::rename(&tmp_path, path) {
|
|
||||||
let _ = fs::remove_file(&tmp_path);
|
|
||||||
return Err(format!("Failed to atomically replace verification history file: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -450,38 +418,4 @@ mod tests {
|
|||||||
assert_eq!(p2.extension().unwrap(), "csv");
|
assert_eq!(p2.extension().unwrap(), "csv");
|
||||||
assert_eq!(p2.to_string_lossy(), "/tmp/history.csv");
|
assert_eq!(p2.to_string_lossy(), "/tmp/history.csv");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_atomic_write_preserves_data_and_cleans_tmp() {
|
|
||||||
let temp_dir = std::env::temp_dir().join("iccery_test_atomic_write");
|
|
||||||
let _ = fs::remove_dir_all(&temp_dir);
|
|
||||||
fs::create_dir_all(&temp_dir).unwrap();
|
|
||||||
|
|
||||||
let file_path = temp_dir.join("verification_history.json");
|
|
||||||
let mut tmp_file_path = file_path.as_os_str().to_os_string();
|
|
||||||
tmp_file_path.push(".tmp");
|
|
||||||
let tmp_file_path = std::path::PathBuf::from(tmp_file_path);
|
|
||||||
|
|
||||||
let rec = sample_record("vr-atom-1", "2026-09-06T12:00:00Z", 0.5);
|
|
||||||
write_history_file(&file_path, &[rec.clone()]).unwrap();
|
|
||||||
|
|
||||||
assert!(file_path.exists(), "Target file must exist");
|
|
||||||
assert!(!tmp_file_path.exists(), "Temporary file must not remain after successful atomic write");
|
|
||||||
|
|
||||||
let loaded = load_history_file(&file_path);
|
|
||||||
assert_eq!(loaded.len(), 1);
|
|
||||||
assert_eq!(loaded[0], rec);
|
|
||||||
|
|
||||||
// Overwrite with updated records to verify atomic replacement
|
|
||||||
let rec2 = sample_record("vr-atom-2", "2026-09-06T12:05:00Z", 1.2);
|
|
||||||
write_history_file(&file_path, &[rec.clone(), rec2.clone()]).unwrap();
|
|
||||||
|
|
||||||
assert!(!tmp_file_path.exists(), "Temporary file must not remain after overwrite");
|
|
||||||
let loaded2 = load_history_file(&file_path);
|
|
||||||
assert_eq!(loaded2.len(), 2);
|
|
||||||
assert_eq!(loaded2[0], rec);
|
|
||||||
assert_eq!(loaded2[1], rec2);
|
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(&temp_dir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ICCery",
|
"productName": "ICCery",
|
||||||
"version": "0.8.4",
|
"version": "0.8.3",
|
||||||
"identifier": "com.gronod.iccery",
|
"identifier": "com.gronod.iccery",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
|
|||||||
Reference in New Issue
Block a user