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
92 changes: 84 additions & 8 deletions app/src-tauri/src/dataclaw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
};

use serde_json::{json, Map, Value};
use tauri::AppHandle;
use tauri::{AppHandle, Emitter};
use tauri_plugin_shell::{process::CommandEvent, ShellExt};
use tokio::time::timeout;

Expand Down Expand Up @@ -200,10 +200,6 @@ fn active_auto_run() -> Value {

pub(crate) fn ensure_auto_enabled_for_run_now() -> Result<(), String> {
let mut config = read_config()?;
let stage = config.get("stage").and_then(Value::as_str);
if !matches!(stage, Some("confirmed") | Some("done")) {
return Err("Run Now requires a confirmed review before publishing.".to_string());
}
if config.get("repo").and_then(Value::as_str).unwrap_or("").trim().is_empty() {
return Err("Run Now requires a configured Hugging Face repo.".to_string());
}
Expand Down Expand Up @@ -267,6 +263,24 @@ pub(crate) fn ensure_auto_enabled_for_run_now() -> Result<(), String> {
write_config(&config)
}

fn auto_export_path() -> Result<PathBuf, String> {
let Some(home) = dirs::home_dir() else {
return Err("Cannot resolve home directory for automated export.".to_string());
};
let dir = home.join(".dataclaw");
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
Ok(dir.join("dataclaw_auto_export.jsonl"))
}

fn emit_auto_progress(app: &AppHandle, msg: &str, phase: &str, extra: Value) {
let payload = json!({
"msg": msg,
"phase": phase,
"extra": extra,
});
let _ = app.emit("logs-line", payload.to_string());
}

pub fn parse_json_block(stdout: &[u8]) -> Result<Value, String> {
let text = String::from_utf8_lossy(stdout);
let payload = if let Some(start) = text.find(JSON_MARKER) {
Expand Down Expand Up @@ -417,16 +431,78 @@ pub fn dataclaw_status() -> Result<Value, String> {
}))
}

pub async fn run_auto_pipeline(app: &AppHandle, publish_attestation: &str) -> Result<Value, String> {
ensure_auto_enabled_for_run_now()?;
let output_path = auto_export_path()?;
let output_path_string = output_path.to_string_lossy().to_string();
emit_auto_progress(
app,
"auto_run_started",
"start",
json!({ "dry_run": false, "source": "configured" }),
);
emit_auto_progress(
app,
"auto_gate_checked",
"gate",
json!({ "privacy_filter_enabled": true, "policy": "configured" }),
);

crate::hf::run_with_token(
app,
&[
"export",
"--no-push",
"--output",
output_path_string.as_str(),
],
)
.await?;

emit_auto_progress(
app,
"auto_confirm_started",
"confirm",
json!({ "file": output_path_string.clone() }),
);
let confirm_result = crate::hf::run_with_token(
app,
&[
"confirm",
"--file",
output_path_string.as_str(),
"--skip-full-name-scan",
"--attest-full-name",
"User skipped full name scan for DataClaw.app automated Run Now.",
"--attest-sensitive",
"User asked DataClaw.app to use configured redactions, privacy filters, company/client/internal name and URL/domain settings; no additional redactions were provided for this automated run.",
"--attest-manual-scan",
"DataClaw.app automated Run Now performed a manual scan equivalent over 20 sessions across beginning, middle, and end using automated review before publishing.",
],
)
.await?;
emit_auto_progress(
app,
"auto_confirm_finished",
"confirm",
json!({
"total_sessions": confirm_result.get("total_sessions").cloned().unwrap_or(Value::Null),
"file_size": confirm_result.get("file_size").cloned().unwrap_or(Value::Null),
}),
);

let args = vec!["export", "--publish-attestation", publish_attestation];
crate::hf::run_with_token(app, &args).await
}

#[tauri::command]
pub async fn dataclaw_auto_now(app: AppHandle, force: bool) -> Result<Value, String> {
ensure_auto_enabled_for_run_now()?;
let attestation = if force {
"User explicitly approved publishing to Hugging Face via DataClaw.app Run Now with force enabled."
} else {
"User explicitly approved publishing to Hugging Face via DataClaw.app Run Now."
};
let args = vec!["export", "--publish-attestation", attestation];
crate::hf::run_with_token(&app, &args).await
run_auto_pipeline(&app, attestation).await
}

#[tauri::command]
Expand Down
9 changes: 2 additions & 7 deletions app/src-tauri/src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,9 @@ async fn run_due_sync(app: AppHandle) -> Result<(), String> {
crate::dataclaw::write_config(&config)?;

let _ = app.emit("dataclaw-scheduled-sync-started", ());
crate::dataclaw::ensure_auto_enabled_for_run_now()?;
match crate::hf::run_with_token(
match crate::dataclaw::run_auto_pipeline(
&app,
&[
"export",
"--publish-attestation",
"User explicitly approved publishing to Hugging Face via DataClaw.app scheduled sync.",
],
"User explicitly approved publishing to Hugging Face via DataClaw.app scheduled sync.",
)
.await
{
Expand Down
2 changes: 2 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Auth from "./routes/Auth";
import Config from "./routes/Config";
import Dashboard from "./routes/Dashboard";
import Logs from "./routes/Logs";
import appIcon from "../src-tauri/icons/icon.png";

const navItems = [
["Dashboard", "/dashboard"],
Expand Down Expand Up @@ -85,6 +86,7 @@ function AppShell() {
{label}
</NavLink>
))}
<img className="nav-logo" src={appIcon} alt="DataClaw" />
</nav>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
Expand Down
64 changes: 62 additions & 2 deletions app/src/routes/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const STAGES = [
{ key: "export", label: "Export" },
{ key: "mechanical_pii", label: "PII scan" },
{ key: "model_privacy", label: "Model privacy" },
{ key: "confirm", label: "Confirm" },
{ key: "push", label: "Upload" },
{ key: "finish", label: "Finish" }
];
Expand All @@ -54,6 +55,53 @@ function errorMessage(error: unknown) {
return error instanceof Error ? error.message : String(error);
}

function sidecarPayload(message: string): JsonObject | null {
const match = message.match(/:\s*(\{[\s\S]*\})\s*\nstderr:/);
if (!match) return null;
try {
return JSON.parse(match[1]) as JsonObject;
} catch {
return null;
}
}

function friendlyRunError(message: string) {
const payload = sidecarPayload(message);
const error = asString(payload?.error);
if (!error) return message;
const hint = asString(payload?.hint);
return hint ? `${error} ${hint}` : error;
}

function progressFromRunError(message: string, startedAtMs: number): ProgressState {
const payload = sidecarPayload(message);
const now = Date.now();
const blockedOn = asString(payload?.blocked_on_step);
const nextCommand = asString(payload?.next_command);
const isBlocked = Boolean(blockedOn) || message.includes("requires a freshly confirmed review") || message.includes("dataclaw confirm");
const detail = friendlyRunError(message);
const metrics = [
metric(isBlocked ? "Blocked on" : "Error", blockedOn ?? "Run Now"),
metric("Next command", nextCommand)
].filter((item): item is { label: string; value: string } => Boolean(item));

return {
runId: "",
stageKey: isBlocked ? "review" : "finish",
stageLabel: isBlocked ? "Review required" : "Failed",
stageIndex: isBlocked ? 4 : STAGES.length - 1,
totalStages: STAGES.length,
detail,
percent: null,
metrics,
nextStages: isBlocked ? ["Confirm", "Upload"] : [],
status: isBlocked ? "blocked" : "failed",
startedAtMs,
updatedAtMs: now,
eventCount: 1
};
}

function asObject(value: unknown): JsonObject | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : null;
}
Expand Down Expand Up @@ -163,6 +211,7 @@ function stageForMessage(msg: string, phase: string) {
if (msg.startsWith("token_count_")) return "export";
if (msg.startsWith("export_")) return "export";
if (msg.startsWith("push_")) return "push";
if (msg.startsWith("auto_confirm_")) return "confirm";
if (msg.startsWith("resolve_export_inputs_")) return "discover";
if (msg === "auto_gate_checked" || phase === "gate") return "gate";
if (msg === "auto_run_started" || phase === "start") return "start";
Expand Down Expand Up @@ -241,6 +290,16 @@ function formatProgressLine(raw: string, previous: ProgressState | null): Progre
metrics.push(metric("Privacy", extra.privacy_filter_enabled ? "on" : "off"));
metrics.push(metric("Policy", extra.policy));
break;
case "auto_confirm_started":
detail = "Confirming automated review";
metrics.push(metric("File", compactPath(extra.file) ?? extra.file));
break;
case "auto_confirm_finished":
detail = "Automated review confirmed";
percent = 100;
metrics.push(metric("Sessions", extra.total_sessions, "0"));
metrics.push(metric("File size", extra.file_size));
break;
case "resolve_export_inputs_started":
detail = "Loading configured project selection";
metrics.push(metric("Excluded", extra.excluded_projects, "0"));
Expand Down Expand Up @@ -668,9 +727,10 @@ export default function Dashboard() {
const summary = pick(result, ["result", "stage"]);
setRunMessage(typeof summary === "string" ? `Result: ${summary}` : "Run complete.");
} catch (err) {
const message = errorMessage(err);
const rawMessage = errorMessage(err);
const message = friendlyRunError(rawMessage);
setError(message);
setProgress((current) => current ? { ...current, status: "failed", detail: message, updatedAtMs: Date.now() } : current);
setProgress((current) => current ? progressFromRunError(rawMessage, current.startedAtMs) : progressFromRunError(rawMessage, startedAtMs));
} finally {
runInFlightRef.current = false;
activeRunRef.current = false;
Expand Down
9 changes: 9 additions & 0 deletions app/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ main {

nav {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
background: var(--panel);
Expand All @@ -83,6 +84,14 @@ nav a.active {
color: white;
}

.nav-logo {
width: 28px;
height: 28px;
margin-left: auto;
object-fit: contain;
flex: 0 0 auto;
}

section.page {
padding: 20px 24px;
overflow: auto;
Expand Down
1 change: 1 addition & 0 deletions app/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
Loading