feat: analytics parity — tool cost, date ranges, budgets, query explorer, replay, snapshots (v2.4.0)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files
Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* @file RemoteSources.tsx
|
||||
* @description Settings UI for the Remote Data Sources feature: manage the SSH
|
||||
* machines this dashboard pulls Claude Code history from, and choose the global
|
||||
* machines this dashboard pulls Claude Code (and, opted in per source, Codex)
|
||||
* history from, and choose the global
|
||||
* "data scope" (which machines' data the whole app shows).
|
||||
*
|
||||
* Backs `server/routes/remote-sources.js` via {@link api.remoteSources} and the
|
||||
@@ -97,6 +98,8 @@ const EMPTY_FORM: RemoteSourceInput = {
|
||||
ssh_port: null,
|
||||
identity_file: "",
|
||||
remote_home: "",
|
||||
sync_codex: false,
|
||||
remote_codex_home: "",
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
@@ -219,6 +222,8 @@ export function RemoteSources() {
|
||||
ssh_port: s.ssh_port,
|
||||
identity_file: s.identity_file || "",
|
||||
remote_home: s.remote_home || "",
|
||||
sync_codex: !!s.sync_codex,
|
||||
remote_codex_home: s.remote_codex_home || "",
|
||||
enabled: s.enabled,
|
||||
});
|
||||
setEditingId(s.id);
|
||||
@@ -242,6 +247,8 @@ export function RemoteSources() {
|
||||
ssh_port: form.ssh_port ? Number(form.ssh_port) : null,
|
||||
identity_file: form.identity_file?.trim() ? form.identity_file.trim() : null,
|
||||
remote_home: form.remote_home?.trim() ? form.remote_home.trim() : null,
|
||||
sync_codex: !!form.sync_codex,
|
||||
remote_codex_home: form.remote_codex_home?.trim() ? form.remote_codex_home.trim() : null,
|
||||
enabled: form.enabled,
|
||||
};
|
||||
try {
|
||||
@@ -591,6 +598,31 @@ export function RemoteSources() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={!!form.sync_codex}
|
||||
onChange={(v) => setForm((f) => ({ ...f, sync_codex: v }))}
|
||||
label={t("remoteSources.fieldSyncCodex", "Also pull Codex sessions")}
|
||||
labelClassName="text-sm text-fg-secondary"
|
||||
/>
|
||||
{form.sync_codex && (
|
||||
<div>
|
||||
<label className="block text-xs text-fg-muted mb-1">
|
||||
{t("remoteSources.fieldRemoteCodexHome", "Remote Codex home (optional)")}
|
||||
</label>
|
||||
<input
|
||||
className="input w-full font-mono"
|
||||
placeholder="~/.codex"
|
||||
value={form.remote_codex_home ?? ""}
|
||||
onChange={(e) => setForm((f) => ({ ...f, remote_codex_home: e.target.value }))}
|
||||
/>
|
||||
<p className="mt-1 text-[11px] text-fg-muted leading-snug">
|
||||
{t(
|
||||
"remoteSources.fieldRemoteCodexHomeHint",
|
||||
"Default ~/.codex (wsl:~/.codex when the Claude home is in WSL). Its sessions/ folder is copied, and the sessions show up as Codex sessions of this source, with their tokens and cost."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Checkbox
|
||||
checked={!!form.enabled}
|
||||
onChange={(v) => setForm((f) => ({ ...f, enabled: v }))}
|
||||
@@ -674,6 +706,9 @@ export function RemoteSources() {
|
||||
{s.host}
|
||||
{s.ssh_port ? `:${s.ssh_port}` : ""}
|
||||
{s.remote_home ? ` · ${s.remote_home}` : ""}
|
||||
{s.sync_codex
|
||||
? ` · ${t("remoteSources.codexOn", "+ Codex")}${s.remote_codex_home ? ` (${s.remote_codex_home})` : ""}`
|
||||
: ""}
|
||||
</div>
|
||||
<div className="text-[11px] text-fg-muted mt-1">
|
||||
{s.last_sync_at
|
||||
@@ -689,9 +724,19 @@ export function RemoteSources() {
|
||||
` · ${t("remoteSources.syncOnRemote", "{{n}} on remote", {
|
||||
n: s.last_sync_counts.sessions_tagged,
|
||||
})}`}
|
||||
{s.last_sync_counts?.codex_sessions != null &&
|
||||
` · ${t("remoteSources.syncCodex", "{{n}} Codex", {
|
||||
n: s.last_sync_counts.codex_sessions,
|
||||
})}`}
|
||||
</div>
|
||||
{s.status === "error" && s.last_error && (
|
||||
<div className="text-[11px] text-status-danger mt-1 break-words">
|
||||
{/* A sync that pulled one CLI's history but not the other's is
|
||||
`ok` with the other's failure in last_error. */}
|
||||
{s.last_error && (s.status === "error" || s.status === "ok") && (
|
||||
<div
|
||||
className={`text-[11px] mt-1 break-words ${
|
||||
s.status === "error" ? "text-status-danger" : "text-status-warning"
|
||||
}`}
|
||||
>
|
||||
{s.last_error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user