Introduces a cron-based scheduler that lets Claude set up recurring and one-time tasks inside containers. Tasks run as separate Claude Code agents and persist across container recreation via the named volume. New files: - container/triple-c-scheduler: CLI for add/remove/enable/disable/list/logs/run/notifications - container/triple-c-task-runner: cron wrapper with flock, logging, notifications, auto-cleanup Key changes: - Dockerfile: add cron package and COPY both scripts - entrypoint.sh: timezone setup, cron daemon, crontab restore, env saving - container.rs: init=true for zombie reaping, TZ env, scheduler instructions, timezone recreation check - image.rs: embed scheduler scripts in build context - app_settings.rs + types.ts: timezone field - settings_commands.rs: detect_host_timezone via iana-time-zone crate - SettingsPanel.tsx: timezone input with auto-detection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
3.4 KiB
Rust
118 lines
3.4 KiB
Rust
use tauri::State;
|
|
|
|
use crate::docker;
|
|
use crate::models::AppSettings;
|
|
use crate::AppState;
|
|
|
|
#[tauri::command]
|
|
pub async fn get_settings(state: State<'_, AppState>) -> Result<AppSettings, String> {
|
|
Ok(state.settings_store.get())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn update_settings(
|
|
settings: AppSettings,
|
|
state: State<'_, AppState>,
|
|
) -> Result<AppSettings, String> {
|
|
state.settings_store.update(settings)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn pull_image(
|
|
image_name: String,
|
|
app_handle: tauri::AppHandle,
|
|
) -> Result<(), String> {
|
|
use tauri::Emitter;
|
|
docker::pull_image(&image_name, move |msg| {
|
|
let _ = app_handle.emit("image-pull-progress", msg);
|
|
})
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn detect_host_timezone() -> Result<String, String> {
|
|
// Try the iana-time-zone crate first (cross-platform)
|
|
match iana_time_zone::get_timezone() {
|
|
Ok(tz) => return Ok(tz),
|
|
Err(e) => log::debug!("iana_time_zone::get_timezone() failed: {}", e),
|
|
}
|
|
|
|
// Fallback: check TZ env var
|
|
if let Ok(tz) = std::env::var("TZ") {
|
|
if !tz.is_empty() {
|
|
return Ok(tz);
|
|
}
|
|
}
|
|
|
|
// Fallback: read /etc/timezone (Linux)
|
|
if let Ok(tz) = std::fs::read_to_string("/etc/timezone") {
|
|
let tz = tz.trim().to_string();
|
|
if !tz.is_empty() {
|
|
return Ok(tz);
|
|
}
|
|
}
|
|
|
|
// Default to UTC if detection fails
|
|
Ok("UTC".to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn detect_aws_config() -> Result<Option<String>, String> {
|
|
if let Some(home) = dirs::home_dir() {
|
|
let aws_dir = home.join(".aws");
|
|
if aws_dir.exists() {
|
|
return Ok(Some(aws_dir.to_string_lossy().to_string()));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn list_aws_profiles() -> Result<Vec<String>, String> {
|
|
let mut profiles = Vec::new();
|
|
|
|
let home = match dirs::home_dir() {
|
|
Some(h) => h,
|
|
None => return Ok(profiles),
|
|
};
|
|
|
|
// Parse ~/.aws/credentials
|
|
let credentials_path = home.join(".aws").join("credentials");
|
|
if credentials_path.exists() {
|
|
if let Ok(contents) = std::fs::read_to_string(&credentials_path) {
|
|
for line in contents.lines() {
|
|
let trimmed = line.trim();
|
|
if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
|
let profile = trimmed[1..trimmed.len() - 1].to_string();
|
|
if !profiles.contains(&profile) {
|
|
profiles.push(profile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse ~/.aws/config (profiles are prefixed with "profile ")
|
|
let config_path = home.join(".aws").join("config");
|
|
if config_path.exists() {
|
|
if let Ok(contents) = std::fs::read_to_string(&config_path) {
|
|
for line in contents.lines() {
|
|
let trimmed = line.trim();
|
|
if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
|
let section = &trimmed[1..trimmed.len() - 1];
|
|
let profile = if let Some(name) = section.strip_prefix("profile ") {
|
|
name.to_string()
|
|
} else {
|
|
section.to_string()
|
|
};
|
|
if !profiles.contains(&profile) {
|
|
profiles.push(profile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(profiles)
|
|
}
|