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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The project is licensed under Apache-2.0. Direct dependencies must be commercial
- Optional ActivityWatch localhost import, enabled only by explicit user action
- Explicit start/stop recording of frontmost macOS applications
- Standard event schema
- URL and window-title masking
- Mandatory raw-field minimization before local storage and export
- Rule-based business labeling
- App usage and business-label duration analysis
- Directly-Follows Graph generation
Expand Down Expand Up @@ -176,12 +176,14 @@ CSV imports support columns such as:
- `activity`
- `timestamp_start`
- `timestamp_end`
- `user`
- `app_name`
- `url`
- `memo`
- `domain`

JSON imports normalize generic arrays and ActivityWatch-style exports into the OpsMineFlow standard event schema.
CSV activity labels must come from an explicit activity column. Alias, window
title, URL, and memo columns are never accepted as labels and are discarded;
a URL can contribute only a normalized domain host for excluded-domain
filtering. JSON imports normalize generic arrays and ActivityWatch-style
exports into the same safe event profile.

## Export Mermaid/SVG/draw.io

Expand All @@ -193,7 +195,7 @@ Runtime data is stored in a local SQLite database under the user's application d

## Privacy and Security

OpsMineFlow does not collect passwords, keystrokes, input text, screenshots, video, audio, or camera data. The standard workflow uses imported event logs and masking before analysis. Exports include a privacy warning and should be reviewed before sharing with clients.
OpsMineFlow does not collect passwords, keystrokes, input text, screenshots, video, audio, or camera data. Before an event is stored, raw aliases, titles, URLs, memos, freeform metadata, and import filenames are removed; case, source, and event IDs become project-scoped opaque references. All API and export formats use this safe profile, and confidential events fail closed at export. Activity labels and application names remain to describe the observed flow, so review them before sharing with clients.

## Disclaimer

Expand Down
45 changes: 6 additions & 39 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,7 @@ function HomeView({
<h2>{t("export.title")}</h2>
<span>{exportPreview ? t("export.bytes", { count: exportPreview.byte_size }) : t("export.localOnly")}</span>
</div>
<p>{t("export.safeDataProfile")}</p>
<div className="inline-fields">
<select
value={exportFormat}
Expand Down Expand Up @@ -1262,22 +1263,7 @@ function HomeView({
<h2>{t("settings.title")}</h2>
<span>{t("settings.days", { count: settingsDraft.retention_days })}</span>
</div>
<label className="check-row">
<input
type="checkbox"
checked={settingsDraft.mask_url_paths}
onChange={(event) => setSettingsDraft({ ...settingsDraft, mask_url_paths: event.target.checked })}
/>
<span>{t("settings.maskUrls")}</span>
</label>
<label className="check-row">
<input
type="checkbox"
checked={settingsDraft.mask_window_titles}
onChange={(event) => setSettingsDraft({ ...settingsDraft, mask_window_titles: event.target.checked })}
/>
<span>{t("settings.maskWindows")}</span>
</label>
<p>{t("settings.safeDataProfile")}</p>
<label className="number-row">
<span>{t("settings.retention")}</span>
<input
Expand Down Expand Up @@ -1708,7 +1694,6 @@ function RecordingPanel({ data, actions, working }: { data: DashboardData; actio
</div>
<RecordingTimeline events={data.events} actions={actions} working={working} />
{!status.available ? <div className="api-warning">{status.remediation || t("recording.unavailable")}</div> : null}
{status.last_error ? <div className="api-warning">{status.last_error}</div> : null}
</section>
);
}
Expand Down Expand Up @@ -1918,15 +1903,12 @@ function findBreakCandidates(events: EventRecord[]): BreakCandidate[] {
}

function RecordingDiagnosticDetails({ status }: { status: RecordingStatus }) {
const { formatDateTime, t } = useI18n();
const { t } = useI18n();
return (
<div className="recording-diagnostic-card">
<Setting label={t("diagnostics.agentVersion")} value={status.agent_version || t("status.unknown")} />
<Setting label={t("diagnostics.agentPath")} value={status.agent_path || "-"} />
<Setting label={t("diagnostics.agentLog")} value={status.log_path || "-"} />
<Setting label={t("diagnostics.heartbeat")} value={status.last_heartbeat_at ? formatDateTime(status.last_heartbeat_at) : t("status.notChecked")} />
<Setting label={t("recording.title")} value={status.available ? t("status.available") : t("status.unavailable")} />
<Setting label={t("diagnostics.captureScope")} value={status.capture_scope} />
<Setting label={t("diagnostics.sessionSafety")} value={t("diagnostics.sessionSafetyValue", { minutes: Math.round((status.token_ttl_seconds || 0) / 60), count: status.rate_limit_per_minute || 0 })} />
<Setting label={t("recording.eventsRecorded")} value={status.recorded_events.toString()} />
{status.remediation ? <p>{status.remediation}</p> : null}
</div>
);
Expand Down Expand Up @@ -2557,22 +2539,7 @@ function SettingsView({ data, actions, working }: { data: DashboardData; actions
<h2>{t("settings.privacy")}</h2>
<span>{t("settings.days", { count: settingsDraft.retention_days })}</span>
</div>
<label className="check-row">
<input
type="checkbox"
checked={settingsDraft.mask_url_paths}
onChange={(event) => setSettingsDraft({ ...settingsDraft, mask_url_paths: event.target.checked })}
/>
<span>{t("settings.maskUrls")}</span>
</label>
<label className="check-row">
<input
type="checkbox"
checked={settingsDraft.mask_window_titles}
onChange={(event) => setSettingsDraft({ ...settingsDraft, mask_window_titles: event.target.checked })}
/>
<span>{t("settings.maskWindows")}</span>
</label>
<p>{t("settings.safeDataProfile")}</p>
<label className="number-row">
<span>{t("settings.retention")}</span>
<input
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"message.previewFailed": "Preview failed",
"message.exportPreviewFailed": "Export preview failed",
"message.diagnosticsFailed": "Diagnostics checks failed",
"message.exportReview": "Review masked fields and confidential flags before sharing this export. Continue?",
"message.exportReview": "Every export uses the safe data profile. Raw URL paths, window titles, aliases, memos, and unknown metadata are excluded. Continue?",
"message.exportCancelled": "Export cancelled.",
"message.imported": "{count} events imported from {source}.",
"message.activityImported": "{count} ActivityWatch events imported. {skipped} duplicates skipped.",
Expand Down Expand Up @@ -120,7 +120,7 @@
"collection.title": "Choose how to add work records",
"collection.body": "Record frontmost app activity directly, or add an existing event log. Collection runs only after an explicit start.",
"collection.fileTitle": "CSV or JSON file",
"collection.fileBody": "Use an exported event log. Preview masking and event counts before import.",
"collection.fileBody": "Use an exported event log. Preview the import mapping and event counts before import; imported records use the safe data profile.",
"collection.activityTitle": "ActivityWatch localhost",
"collection.activityBody": "Import only after collection scope and participant consent are confirmed.",
"collection.autoTitle": "Built-in Mac app recording",
Expand All @@ -141,7 +141,7 @@
"onboarding.analyzeBody": "Review totals, time, bottlenecks, and automation candidates.",
"onboarding.analyzeDisabled": "Available after data is added.",
"onboarding.exportTitle": "Export results",
"onboarding.exportBody": "Review masking and save locally.",
"onboarding.exportBody": "Preview the safe data profile and save locally.",
"onboarding.exportDisabled": "Available after data is added.",
"recording.kicker": "Native macOS recording",
"recording.title": "Record work",
Expand Down Expand Up @@ -291,11 +291,11 @@
"export.title": "Exports",
"export.localOnly": "Local files only",
"export.bytes": "{count} bytes",
"export.safeDataProfile": "Safe data only — raw URL paths, window titles, aliases, memos, and unknown metadata are never shown or exported.",
"settings.title": "Settings",
"settings.privacy": "Privacy Controls",
"settings.days": "{count} days",
"settings.maskUrls": "Mask URL paths",
"settings.maskWindows": "Mask window titles",
"settings.safeDataProfile": "Safe data profile is always on. Raw URL paths and window titles cannot be shown or exported.",
"settings.retention": "Retention days",
"settings.sessionGap": "New-session gap (minutes)",
"settings.excludedApps": "Excluded apps",
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"message.previewFailed": "確認用データの作成に失敗した",
"message.exportPreviewFailed": "出力内容の確認に失敗した",
"message.diagnosticsFailed": "診断に失敗した",
"message.exportReview": "共有前にマスキング項目と機密フラグを確認した? 続行する?",
"message.exportReview": "すべての出力は安全なデータプロファイルを使う。生のURLパス・ウィンドウタイトル・別名・メモ・未定義メタデータは除外される。続行する?",
"message.exportCancelled": "出力をキャンセルした。",
"message.imported": "{source} から {count} 件を取り込んだ。",
"message.activityImported": "ActivityWatchから {count} 件を取り込んだ。重複 {skipped} 件はスキップした。",
Expand Down Expand Up @@ -120,7 +120,7 @@
"collection.title": "業務記録の追加方法を選ぶ",
"collection.body": "前面アプリの活動を直接記録するか、既存のイベントログを追加する。明示的に開始した間だけ収集する。",
"collection.fileTitle": "CSVまたはJSONファイル",
"collection.fileBody": "出力済みイベントログを使う。取り込み前に件数とマスキング結果を確認できる。",
"collection.fileBody": "出力済みイベントログを使う。取り込み前に対応付けとイベント件数を確認し、取り込み後の記録は安全なデータプロファイルを使う。",
"collection.activityTitle": "ActivityWatch localhost",
"collection.activityBody": "収集範囲と参加者の同意を確認した場合だけ取り込む。",
"collection.autoTitle": "Macアプリの組み込み記録",
Expand All @@ -141,7 +141,7 @@
"onboarding.analyzeBody": "件数、時間、ボトルネック、自動化候補を見る。",
"onboarding.analyzeDisabled": "データ追加後に使える。",
"onboarding.exportTitle": "結果を出力",
"onboarding.exportBody": "マスキングを確認してローカル保存する。",
"onboarding.exportBody": "安全なデータプロファイルを確認してローカル保存する。",
"onboarding.exportDisabled": "データ追加後に使える。",
"recording.kicker": "macOSネイティブ記録",
"recording.title": "業務を記録",
Expand Down Expand Up @@ -291,11 +291,11 @@
"export.title": "出力",
"export.localOnly": "ローカルファイルのみ",
"export.bytes": "{count} バイト",
"export.safeDataProfile": "安全なデータだけを使用 — 生のURLパス・ウィンドウタイトル・別名・メモ・未定義メタデータは表示も出力もしない。",
"settings.title": "設定",
"settings.privacy": "プライバシー設定",
"settings.days": "{count} 日",
"settings.maskUrls": "URLパスをマスキング",
"settings.maskWindows": "ウィンドウタイトルをマスキング",
"settings.safeDataProfile": "安全なデータプロファイルは常に有効。生のURLパスとウィンドウタイトルは表示も出力もできない。",
"settings.retention": "保持日数",
"settings.sessionGap": "新しいセッションに分ける間隔(分)",
"settings.excludedApps": "除外するアプリ",
Expand Down
12 changes: 1 addition & 11 deletions apps/desktop/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export type Summary = {
period_end: string;
app_usage_seconds: Record<string, number>;
label_usage_seconds: Record<string, number>;
user_usage_seconds: Record<string, number>;
average_event_duration_seconds: number;
analysis_receipt: AnalysisReceipt;
};
Expand Down Expand Up @@ -251,28 +250,19 @@ export type RecordingStatus = {
installed: boolean;
available: boolean;
remediation: string;
agent_path: string;
agent_version: string;
log_path: string;
token_ttl_seconds: number;
rate_limit_per_minute: number;
active: boolean;
paused: boolean;
session_id: string;
case_id: string;
activity_label: string;
started_at: string;
paused_at: string;
pause_reason: string;
pause_intervals: Array<{
started_at: string;
ended_at: string;
reason: string;
}>;
current_app: string;
recorded_events: number;
last_heartbeat_at: string;
last_error: string;
capture_ended: boolean;
capture_scope: "frontmost_app_only";
};

Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/test/i18n.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ test("beginner workflow labels are explicit in both languages", () => {
assert.match(appSource, /PrivacyEvidencePanel/);
});

test("privacy controls cannot turn off the safe data profile", () => {
assert.match(appSource, /t\("export\.safeDataProfile"\)/);
assert.match(appSource, /t\("settings\.safeDataProfile"\)/);
assert.match(appSource, /t\("message\.exportReview"\)/);
assert.doesNotMatch(appSource, /settings\.maskUrls|settings\.maskWindows/);
assert.doesNotMatch(appSource, /checked=\{settingsDraft\.(mask_url_paths|mask_window_titles)\}/);
assert.match(en["export.safeDataProfile"], /raw URL paths.*never shown or exported/);
assert.match(en["message.exportReview"], /safe data profile/);
assert.match(ja["settings.safeDataProfile"], /常に有効/);
});

test("the packaged WebUI uses the allowlisted Tauri proxy instead of a direct local API session", () => {
assert.match(apiSource, /invoke<T>\("local_api_operation"/);
assert.match(apiSource, /invoke<\{ deleted: boolean \}>\("delete_local_data", \{ payload: withProjectScope/);
Expand Down
3 changes: 2 additions & 1 deletion docs/OPEN_QUESTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ See [product/COLLECTION_ROADMAP.md](product/COLLECTION_ROADMAP.md) for the full

- Confirm whether SVG export should use a bundled local renderer or remain deferred while Mermaid and draw.io exports are available.
- Define the minimum evidence required to promote each collector from technical preview to controlled beta.
- Define the user-facing preview, confirmation, retention, and deletion receipt for project data versus workspace migration snapshots before the all-data lifecycle work is released (tracked by #52 and #54).
- Define the user-facing preview, confirmation, retention, and deletion receipt for project data versus encrypted recovery artifacts before the all-data lifecycle work is released (tracked by #52 and #54).
- Define encrypted retention and deletion guarantees for recovery artifacts. Pre-v4 migration no longer creates a plaintext recovery snapshot; #52, #53, and #54 still define future encrypted backup and deletion lifecycle behavior.
15 changes: 12 additions & 3 deletions docs/architecture/DATA_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ Persistent databases use `PRAGMA user_version` together with an append-only `sch

Schema version 3 introduced project isolation. It atomically rebuilds the scoped tables, creates a deterministic opaque `Migrated data` project for existing records, backfills every legacy row into that project, and records before/after row counts and content fingerprints in workspace metadata. A failed upgrade leaves the prior schema intact; a retry starts from the original legacy state rather than a partial project migration.

Before upgrading an existing recognized database, the app creates a SQLite online-backup snapshot in the local `backups/` directory. The backup directory is owner-only and the snapshot file is owner-read/write only. The app retains at most the three newest migration snapshots after an attempt that created a snapshot, whether that attempt commits or rolls back. The app verifies database integrity and foreign-key consistency before and after migration, then checkpoints WAL after a successful upgrade. A post-commit WAL checkpoint warning does not roll back a completed schema migration; diagnostics reports it separately for follow-up.

If a database was created by a newer app version, has an unknown migration ledger, or is not a recognized legacy schema, OpsMineFlow fails closed. It does not create tables, overwrite the database, seed sample data, or attempt an automatic restore. Keep the original database and use the pre-upgrade snapshot for manual recovery with a compatible build. Clearing a project never removes workspace-level migration snapshots; backup retention and all-data deletion are separately defined lifecycle operations. Filesystem or Time Machine backups are outside the app's control.
Schema version 4 rewrites each project-scoped event payload through the safe
event allowlist and replaces import-history filenames with generic import
types. It clears freeform automation-review notes and derives case, source,
and event identifiers with a project-scoped HMAC using a local owner-only key.
This migration is idempotent for already safe identifiers and runs in the same
startup transaction as the migration ledger update. The database stores a
non-secret HMAC verifier for that key; a missing or mismatched key fails closed
rather than being regenerated for an existing dataset.

Before upgrading a recognized pre-v4 database, the app rewrites it in one SQLite transaction without creating a plaintext pre-upgrade snapshot. A failed migration leaves the original database untouched; a successful migration leaves only the minimized database. The app verifies database integrity and foreign-key consistency before and after migration, then securely compacts and checkpoints WAL. If that final privacy cleanup cannot be verified, startup fails closed. Encrypted backup and recovery lifecycle policy are tracked separately.

If a database was created by a newer app version, has an unknown migration ledger, or is not a recognized legacy schema, OpsMineFlow fails closed. It does not create tables, overwrite the database, seed sample data, or attempt an automatic restore. Keep the original database and use a compatible build to recover it. Encrypted backup retention and all-data deletion are separately defined lifecycle operations. Filesystem or Time Machine backups are outside the app's control.
Loading