+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "iccery",
|
||||
"private": true,
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "iccery"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
||||
authors = ["Gordon"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -128,6 +128,91 @@ pub fn get_default_working_dir(app: AppHandle) -> Result<String, String> {
|
||||
resolve_safe_cwd(&app, "")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn log_frontend_message(level: String, message: String, context: Option<String>) {
|
||||
let ctx = context.map(|c| format!("[{}] ", c)).unwrap_or_default();
|
||||
match level.to_lowercase().as_str() {
|
||||
"error" => log::error!(target: "frontend", "{}{}", ctx, message),
|
||||
"warn" | "warning" => log::warn!(target: "frontend", "{}{}", ctx, message),
|
||||
"debug" => log::debug!(target: "frontend", "{}{}", ctx, message),
|
||||
"trace" => log::trace!(target: "frontend", "{}{}", ctx, message),
|
||||
_ => log::info!(target: "frontend", "{}{}", ctx, message),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prune_historical_logs(log_dir: &std::path::Path, max_history: usize) {
|
||||
if let Ok(entries) = std::fs::read_dir(log_dir) {
|
||||
let mut rotated_files: Vec<(std::path::PathBuf, std::time::SystemTime)> = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
// Identify rotated log files (e.g. iccery_2026-08-30.log, iccery.log.1, etc.)
|
||||
if name.starts_with("iccery") && name != "iccery.log" {
|
||||
let mtime = entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);
|
||||
rotated_files.push((path, mtime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort newest first
|
||||
rotated_files.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
// Delete any beyond max_history
|
||||
if rotated_files.len() > max_history {
|
||||
for (old_file, _) in &rotated_files[max_history..] {
|
||||
log::info!(target: "log", "Pruning old log segment: {:?}", old_file);
|
||||
let _ = std::fs::remove_file(old_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_recent_log_lines(content: &str, max_lines: usize) -> String {
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
if lines.len() <= max_lines {
|
||||
content.to_string()
|
||||
} else {
|
||||
lines[lines.len() - max_lines..].join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_recent_log_excerpt(
|
||||
app: AppHandle,
|
||||
max_lines: Option<usize>,
|
||||
max_bytes: Option<usize>,
|
||||
) -> Result<String, String> {
|
||||
let log_dir = app
|
||||
.path()
|
||||
.app_log_dir()
|
||||
.map_err(|e| format!("Failed to resolve log dir: {}", e))?;
|
||||
let log_file = log_dir.join("iccery.log");
|
||||
|
||||
if !log_file.exists() {
|
||||
return Ok("No log file found yet.".to_string());
|
||||
}
|
||||
|
||||
let max_b = max_bytes.unwrap_or(65536); // 64 KiB cap
|
||||
let max_l = max_lines.unwrap_or(200);
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
let mut file = std::fs::File::open(&log_file).map_err(|e| format!("Failed to open log file: {}", e))?;
|
||||
let file_len = file.metadata().map_err(|e| e.to_string())?.len();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
if file_len > max_b as u64 {
|
||||
file.seek(SeekFrom::End(-(max_b as i64)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
file.read_to_end(&mut buf).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
file.read_to_end(&mut buf).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&buf);
|
||||
Ok(extract_recent_log_lines(&text, max_l))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_log_path(app: AppHandle) -> Result<String, String> {
|
||||
let log_dir = app
|
||||
@@ -1496,4 +1581,42 @@ mod tests {
|
||||
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_recent_log_lines() {
|
||||
let content = "line 1\nline 2\nline 3\nline 4\nline 5";
|
||||
assert_eq!(extract_recent_log_lines(content, 2), "line 4\nline 5");
|
||||
assert_eq!(extract_recent_log_lines(content, 10), content);
|
||||
assert_eq!(extract_recent_log_lines("", 5), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prune_historical_logs() {
|
||||
let temp_dir = std::env::temp_dir().join(format!("iccery_test_logs_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
|
||||
std::fs::create_dir_all(&temp_dir).unwrap();
|
||||
|
||||
// Create main log
|
||||
std::fs::write(temp_dir.join("iccery.log"), b"active log").unwrap();
|
||||
|
||||
// Create 8 historical log files with slight pauses to ensure distinct mtimes
|
||||
for i in 1..=8 {
|
||||
let file_path = temp_dir.join(format!("iccery_2026-08-0{}.log", i));
|
||||
std::fs::write(&file_path, format!("old log {}", i)).unwrap();
|
||||
}
|
||||
|
||||
prune_historical_logs(&temp_dir, 5);
|
||||
|
||||
// Count remaining historical files
|
||||
let mut count = 0;
|
||||
for entry in std::fs::read_dir(&temp_dir).unwrap().flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("iccery_") {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(count, 5);
|
||||
assert!(temp_dir.join("iccery.log").exists(), "Active log must never be deleted");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -19,14 +19,20 @@ pub fn run() {
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
|
||||
])
|
||||
.level(if cfg!(debug_assertions) {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
})
|
||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
|
||||
.max_file_size(5 * 1024 * 1024)
|
||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
|
||||
.build(),
|
||||
)
|
||||
.setup(|app| {
|
||||
if let Ok(log_dir) = app.path().app_log_dir() {
|
||||
commands::prune_historical_logs(&log_dir, 5);
|
||||
}
|
||||
let settings = settings::load_settings(app.handle().clone()).unwrap_or_default();
|
||||
let filter = settings::parse_log_level_filter(settings.log_level.as_deref());
|
||||
log::set_max_level(filter);
|
||||
log::info!("ICCery initialized. Effective log level: {:?}", filter);
|
||||
Ok(())
|
||||
})
|
||||
.manage(process_manager::ProcessManager::new())
|
||||
.manage(print::PrinterDevModeStore::new())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -34,6 +40,8 @@ pub fn run() {
|
||||
commands::get_app_info,
|
||||
commands::get_default_working_dir,
|
||||
commands::get_log_path,
|
||||
commands::get_recent_log_excerpt,
|
||||
commands::log_frontend_message,
|
||||
commands::open_log_dir,
|
||||
commands::parse_ti2_header,
|
||||
commands::select_existing_target,
|
||||
|
||||
@@ -8,6 +8,57 @@ use tokio::sync::Mutex;
|
||||
|
||||
use crate::events::{emit_error, emit_stderr, emit_stdout};
|
||||
|
||||
pub fn get_user_home_dir() -> Option<String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::env::var("USERPROFILE")
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
let drive = std::env::var("HOMEDRIVE").ok()?;
|
||||
let path = std::env::var("HOMEPATH").ok()?;
|
||||
Some(format!("{}{}", drive, path))
|
||||
})
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
std::env::var("HOME").ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sanitize_arg_for_logging(arg: &str) -> String {
|
||||
if let Some(home) = get_user_home_dir() {
|
||||
if !home.trim().is_empty() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let arg_lower = arg.to_lowercase();
|
||||
let home_lower = home.to_lowercase();
|
||||
if let Some(pos) = arg_lower.find(&home_lower) {
|
||||
let mut result = String::new();
|
||||
result.push_str(&arg[..pos]);
|
||||
result.push('~');
|
||||
result.push_str(&arg[pos + home.len()..]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
if let Some(pos) = arg.find(&home) {
|
||||
let mut result = String::new();
|
||||
result.push_str(&arg[..pos]);
|
||||
result.push('~');
|
||||
result.push_str(&arg[pos + home.len()..]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
arg.to_string()
|
||||
}
|
||||
|
||||
pub fn sanitize_args_for_logging(args: &[String]) -> Vec<String> {
|
||||
args.iter().map(|a| sanitize_arg_for_logging(a)).collect()
|
||||
}
|
||||
|
||||
pub struct ProcessManager {
|
||||
stdins: Arc<Mutex<HashMap<String, Arc<Mutex<ChildStdin>>>>>,
|
||||
killers: Arc<Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
|
||||
@@ -37,7 +88,7 @@ impl ProcessManager {
|
||||
}
|
||||
let mut command = Command::new(&binary);
|
||||
command.args(&args);
|
||||
if let Some(dir) = cwd {
|
||||
if let Some(ref dir) = cwd {
|
||||
command.current_dir(dir);
|
||||
}
|
||||
command.stdout(Stdio::piped());
|
||||
@@ -51,7 +102,21 @@ impl ProcessManager {
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
log::info!(target: "subprocess", "Spawning process '{id}': {binary} {:?}", args);
|
||||
let sanitized_binary = sanitize_arg_for_logging(&binary);
|
||||
let sanitized_args = sanitize_args_for_logging(&args);
|
||||
let sanitized_cwd = cwd.as_deref().map(sanitize_arg_for_logging);
|
||||
log::info!(
|
||||
target: "subprocess",
|
||||
"Spawning process '{id}': {sanitized_binary} {:?} (cwd: {:?})",
|
||||
sanitized_args,
|
||||
sanitized_cwd
|
||||
);
|
||||
log::debug!(
|
||||
target: "subprocess",
|
||||
"Raw spawn invocation for '{id}': {binary} {:?} (cwd: {:?})",
|
||||
args,
|
||||
cwd
|
||||
);
|
||||
match command.spawn() {
|
||||
Ok(mut child) => {
|
||||
let stdout = child.stdout.take().expect("Failed to open stdout");
|
||||
@@ -244,4 +309,18 @@ mod tests {
|
||||
assert!(killers.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_arg_for_logging() {
|
||||
if let Some(home) = get_user_home_dir() {
|
||||
if !home.is_empty() {
|
||||
let test_path = format!("{}/test_file.ti1", home);
|
||||
let sanitized = sanitize_arg_for_logging(&test_path);
|
||||
assert!(sanitized.starts_with('~'));
|
||||
assert!(!sanitized.contains(&home));
|
||||
}
|
||||
}
|
||||
let safe_arg = "-v";
|
||||
assert_eq!(sanitize_arg_for_logging(safe_arg), "-v");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,23 @@ pub fn get_default_presets() -> Vec<ProfilingPreset> {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn parse_log_level_filter(level: Option<&str>) -> log::LevelFilter {
|
||||
match level.map(|s| s.trim().to_lowercase()).as_deref() {
|
||||
Some("error") => log::LevelFilter::Error,
|
||||
Some("warn") | Some("warning") => log::LevelFilter::Warn,
|
||||
Some("info") => log::LevelFilter::Info,
|
||||
Some("debug") => log::LevelFilter::Debug,
|
||||
Some("trace") => log::LevelFilter::Trace,
|
||||
_ => {
|
||||
if cfg!(debug_assertions) {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_settings(app: AppHandle) -> Result<AppSettings, String> {
|
||||
let path = app.path().app_data_dir().unwrap().join("settings.json");
|
||||
@@ -181,7 +198,14 @@ pub fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String
|
||||
let path = app.path().app_data_dir().unwrap();
|
||||
fs::create_dir_all(&path).map_err(|e| e.to_string())?;
|
||||
let json = serde_json::to_string_pretty(&settings).unwrap();
|
||||
fs::write(path.join("settings.json"), json).map_err(|e| e.to_string())
|
||||
fs::write(path.join("settings.json"), json).map_err(|e| e.to_string())?;
|
||||
|
||||
// Dynamically apply the log level immediately at runtime
|
||||
let filter = parse_log_level_filter(settings.log_level.as_deref());
|
||||
log::set_max_level(filter);
|
||||
log::info!("Applied log level filter: {:?}", filter);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -304,4 +328,24 @@ mod tests {
|
||||
let imported = import_preset_json(json).expect("Import failed");
|
||||
assert_eq!(preset, imported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_log_level_filter() {
|
||||
assert_eq!(parse_log_level_filter(Some("error")), log::LevelFilter::Error);
|
||||
assert_eq!(parse_log_level_filter(Some("ERROR")), log::LevelFilter::Error);
|
||||
assert_eq!(parse_log_level_filter(Some("warn")), log::LevelFilter::Warn);
|
||||
assert_eq!(parse_log_level_filter(Some("warning")), log::LevelFilter::Warn);
|
||||
assert_eq!(parse_log_level_filter(Some("info")), log::LevelFilter::Info);
|
||||
assert_eq!(parse_log_level_filter(Some("debug")), log::LevelFilter::Debug);
|
||||
assert_eq!(parse_log_level_filter(Some("trace")), log::LevelFilter::Trace);
|
||||
assert_eq!(parse_log_level_filter(Some(" Info ")), log::LevelFilter::Info);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
assert_eq!(parse_log_level_filter(None), log::LevelFilter::Debug);
|
||||
assert_eq!(parse_log_level_filter(Some("unknown")), log::LevelFilter::Debug);
|
||||
} else {
|
||||
assert_eq!(parse_log_level_filter(None), log::LevelFilter::Info);
|
||||
assert_eq!(parse_log_level_filter(Some("unknown")), log::LevelFilter::Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ICCery",
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.4",
|
||||
"identifier": "com.gronod.iccery",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
|
||||
+4
-2
@@ -691,10 +691,12 @@
|
||||
<option value="debug">Debug (Detailed)</option>
|
||||
<option value="trace">Trace (Verbose)</option>
|
||||
</select>
|
||||
<small class="help-hint" style="display:block; font-size:0.75rem; color:var(--text-muted, #888); margin-top:3px;">Applies immediately on Save</small>
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-end; gap: 8px;">
|
||||
<div style="display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap;">
|
||||
<button type="button" id="btnOpenLogFolder" class="secondary" title="Open log folder in file explorer">📂 Open Log Folder</button>
|
||||
<button type="button" id="btnCopyLogPath" class="secondary" title="Copy log path to clipboard">📋 Copy Path</button>
|
||||
<button type="button" id="btnCopyLogPath" class="secondary" title="Copy full log file path to clipboard">📋 Copy Path</button>
|
||||
<button type="button" id="btnCopyLogExcerpt" class="secondary" title="Copy recent log entries (last 200 lines) to clipboard">📋 Copy Recent Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
<small id="logPathDisplay" class="path-display" style="margin-top: 6px; display: block;"></small>
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import { initSettings } from './settings.js';
|
||||
import { initGamutViewer } from './gamut_viewer.js';
|
||||
import { initPresets } from './presets.js';
|
||||
import { wizardState } from './state.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
|
||||
@@ -73,7 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
initFn();
|
||||
} catch (e) {
|
||||
console.error(`[ICCery Init Error] Failed to initialize ${name}:`, e);
|
||||
logger.error(`Failed to initialize ${name}: ${e}`, 'AppInit');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const { listen } = window.__TAURI__.event;
|
||||
import { startSwatchListener, stopSwatchListener } from './swatch_grid.js';
|
||||
import { setStage3Result } from './colprof.js';
|
||||
import { wizardState } from './state.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
// Module-level state: set by Stage 2 when it completes
|
||||
let stage2Basename = "";
|
||||
|
||||
@@ -3,6 +3,7 @@ const { listen } = window.__TAURI__.event;
|
||||
import { setStage4Result } from './profcheck.js';
|
||||
import { loadGamutMesh } from './gamut_viewer.js';
|
||||
import { wizardState } from './state.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
let chartreadBasename = "";
|
||||
let chartreadCwd = "";
|
||||
@@ -130,6 +131,7 @@ export function initColprof() {
|
||||
|
||||
await invoke("run_colprof", { config });
|
||||
} catch (err) {
|
||||
logger.error(`run_colprof invocation failed: ${err}`, 'Stage4-Colprof');
|
||||
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||
spinnerContainer.classList.add("hidden");
|
||||
btnCreateProfile.disabled = false;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
|
||||
/**
|
||||
* Lightweight frontend logger that writes to both the browser console and the
|
||||
* rotating host application log (iccery.log) via Tauri command `log_frontend_message`.
|
||||
*/
|
||||
export const logger = {
|
||||
async log(level, message, context = null) {
|
||||
const formattedMsg = message instanceof Error ? `${message.message}\n${message.stack || ''}` : String(message);
|
||||
|
||||
// DevTools console output
|
||||
if (level === 'error') {
|
||||
console.error(`[${context || 'Frontend'}]`, formattedMsg);
|
||||
} else if (level === 'warn' || level === 'warning') {
|
||||
console.warn(`[${context || 'Frontend'}]`, formattedMsg);
|
||||
} else if (level === 'debug') {
|
||||
console.debug(`[${context || 'Frontend'}]`, formattedMsg);
|
||||
} else {
|
||||
console.log(`[${context || 'Frontend'}]`, formattedMsg);
|
||||
}
|
||||
|
||||
// Forward to backend log
|
||||
try {
|
||||
await invoke('log_frontend_message', {
|
||||
level: level.toLowerCase(),
|
||||
message: formattedMsg,
|
||||
context: context || null,
|
||||
});
|
||||
} catch (_) {
|
||||
// Avoid recursion or noise if invoke fails
|
||||
}
|
||||
},
|
||||
|
||||
error(message, context) {
|
||||
return this.log('error', message, context);
|
||||
},
|
||||
|
||||
warn(message, context) {
|
||||
return this.log('warn', message, context);
|
||||
},
|
||||
|
||||
info(message, context) {
|
||||
return this.log('info', message, context);
|
||||
},
|
||||
|
||||
debug(message, context) {
|
||||
return this.log('debug', message, context);
|
||||
}
|
||||
};
|
||||
@@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
import { setStage2Result } from './chartread.js';
|
||||
import { wizardState } from './state.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
// Module-level state: set by Stage 1 when it completes
|
||||
let stage1Basename = "";
|
||||
@@ -463,6 +464,7 @@ export function initPrinttarg() {
|
||||
await invoke("run_printtarg", { config });
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`run_printtarg invocation failed: ${err}`, 'Stage2-Printtarg');
|
||||
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||
btnCreateLayout.disabled = false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
import { loadGamutMesh } from './gamut_viewer.js';
|
||||
import { wizardState } from './state.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
let profileBasename = "";
|
||||
let profileCwd = "";
|
||||
@@ -106,6 +107,7 @@ export function initProfcheck() {
|
||||
|
||||
await invoke("run_profcheck", { config });
|
||||
} catch (err) {
|
||||
logger.error(`run_profcheck invocation failed: ${err}`, 'Stage5-Profcheck');
|
||||
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||
btnVerify.disabled = false;
|
||||
}
|
||||
|
||||
+31
-3
@@ -1,4 +1,5 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export async function initSettings() {
|
||||
const dialog = document.getElementById('settingsDialog');
|
||||
@@ -8,6 +9,7 @@ export async function initSettings() {
|
||||
const logLevelSelect = document.getElementById('logLevelSelect');
|
||||
const btnOpenLogFolder = document.getElementById('btnOpenLogFolder');
|
||||
const btnCopyLogPath = document.getElementById('btnCopyLogPath');
|
||||
const btnCopyLogExcerpt = document.getElementById('btnCopyLogExcerpt');
|
||||
const logPathDisplay = document.getElementById('logPathDisplay');
|
||||
|
||||
if (!dialog || !openBtn) return;
|
||||
@@ -18,7 +20,7 @@ export async function initSettings() {
|
||||
const path = await invoke('get_log_path');
|
||||
logPathDisplay.textContent = path ? `Log file: ${path}` : '';
|
||||
} catch (err) {
|
||||
console.warn("Could not retrieve log path:", err);
|
||||
logger.warn(`Could not retrieve log path: ${err}`, 'Settings');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +35,7 @@ export async function initSettings() {
|
||||
await refreshLogPath();
|
||||
dialog.showModal();
|
||||
} catch (e) {
|
||||
console.error("Failed to load settings:", e);
|
||||
logger.error(`Failed to load settings: ${e}`, 'Settings');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,6 +44,7 @@ export async function initSettings() {
|
||||
try {
|
||||
await invoke('open_log_dir');
|
||||
} catch (err) {
|
||||
logger.error(`Failed to open log folder: ${err}`, 'Settings');
|
||||
alert(`Failed to open log folder: ${err}`);
|
||||
}
|
||||
});
|
||||
@@ -60,11 +63,35 @@ export async function initSettings() {
|
||||
}, 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to copy log path: ${err}`, 'Settings');
|
||||
alert(`Failed to copy log path: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnCopyLogExcerpt) {
|
||||
btnCopyLogExcerpt.addEventListener('click', async () => {
|
||||
try {
|
||||
const excerpt = await invoke('get_recent_log_excerpt', {
|
||||
maxLines: 200,
|
||||
maxBytes: 65536,
|
||||
});
|
||||
if (excerpt) {
|
||||
await navigator.clipboard.writeText(excerpt);
|
||||
const lineCount = excerpt.split('\n').length;
|
||||
const originalText = btnCopyLogExcerpt.textContent;
|
||||
btnCopyLogExcerpt.textContent = `✓ Copied (${lineCount} lines)!`;
|
||||
setTimeout(() => {
|
||||
btnCopyLogExcerpt.textContent = originalText;
|
||||
}, 2500);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to copy log excerpt: ${err}`, 'Settings');
|
||||
alert(`Failed to copy log excerpt: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => dialog.close());
|
||||
}
|
||||
@@ -80,9 +107,10 @@ export async function initSettings() {
|
||||
log_level: logLevelSelect ? logLevelSelect.value : (currentSettings.log_level || 'info'),
|
||||
};
|
||||
await invoke('save_settings', { settings });
|
||||
logger.info(`Settings saved. Log level set to: ${settings.log_level}`, 'Settings');
|
||||
dialog.close();
|
||||
} catch (e) {
|
||||
console.error("Failed to save settings:", e);
|
||||
logger.error(`Failed to save settings: ${e}`, 'Settings');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const { listen } = window.__TAURI__.event;
|
||||
import { setStage1Result } from './printtarg.js';
|
||||
import { populateStage3TargetContext } from './chartread.js';
|
||||
import { wizardState } from './state.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export function initTargen() {
|
||||
const colourSpaceRadios = document.querySelectorAll('input[name="colourSpace"]');
|
||||
@@ -351,6 +352,7 @@ export function initTargen() {
|
||||
if (logPre) logPre.textContent = "Starting targen...\n";
|
||||
await invoke("run_targen", { config });
|
||||
} catch (err) {
|
||||
logger.error(`run_targen invocation failed: ${err}`, 'Stage1-Targen');
|
||||
if (logPre) logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||
btnGenerate.disabled = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user