(null)
+ const [databaseError, setDatabaseError] = useState('')
const error = localError || externalServicesError || ''
const refreshInFlightRef = useRef(false)
const actionInFlightRef = useRef(false)
@@ -1095,6 +1101,9 @@ function ServicesSection({
}
}
+ const serviceIsRunning = (service: DesktopServiceReport['services'][number]) =>
+ service.state === 'running' || (service.loaded && service.state === null)
+
const serviceAction = async (
service: DesktopServiceReport['services'][number],
action: 'start' | 'stop' | 'restart'
@@ -1200,6 +1209,35 @@ function ServicesSection({
}
}
+ const databaseAction = async (action: 'backup' | 'restore') => {
+ if (dirty) {
+ setDatabaseError('Save or discard draft changes before using database recovery.')
+ return
+ }
+ const activeServices = report?.services.filter((service) => serviceIsRunning(service)) ?? []
+ if (action === 'restore' && (report?.supported !== true || activeServices.length > 0)) {
+ setDatabaseError('Stop all Cortana services before restoring a database snapshot.')
+ return
+ }
+ const confirmation =
+ action === 'backup'
+ ? 'Export a verified Cortana database snapshot?\n\nThe native picker will choose a new .sqlite3 file. No sync or service is started.'
+ : 'Restore this Cortana database snapshot?\n\nThis replaces the active index and keeps a pre-restore recovery copy. All Cortana services must already be stopped. No sync is run.'
+ if (!window.confirm(confirmation)) return
+ setDatabaseBusy(action)
+ setDatabaseResult(null)
+ setDatabaseError('')
+ try {
+ const result =
+ action === 'backup' ? await backupDesktopDatabase() : await restoreDesktopDatabase()
+ if (result) setDatabaseResult(result)
+ } catch (caught) {
+ setDatabaseError(caught instanceof Error ? caught.message : `Database ${action} failed`)
+ } finally {
+ setDatabaseBusy('')
+ }
+ }
+
const install = async () => {
if (
!window.confirm(
@@ -1330,7 +1368,8 @@ function ServicesSection({
report?.supported === true &&
report.services.some((service) => service.name === 'sync' && !service.installed)
const syncScheduleNeedsApply = needsSyncInstall || scheduleApplyPending
- const actionInFlight = Boolean(busy) || serviceActivity?.status === 'running'
+ const actionInFlight =
+ Boolean(busy) || Boolean(databaseBusy) || serviceActivity?.status === 'running'
const actionMessage = serviceActivity
? `${serviceActivity.action === 'install' ? 'Install' : serviceActivity.action[0].toUpperCase() + serviceActivity.action.slice(1)} ${serviceActivity.target}${serviceActivity.status === 'running' ? ' in progress…' : serviceActivity.status === 'succeeded' ? ' completed.' : ` failed: ${serviceActivity.detail || 'unknown error'}`}`
: ''
@@ -1487,6 +1526,66 @@ function ServicesSection({
)}
+
+
+
Database recovery
+
+ Export a verified SQLite snapshot or restore one into the active index. Restore is
+ blocked while any Cortana service is running and never starts recurring sync.
+
+
+
+
+
+
+
+ {(databaseResult || databaseError) && (
+
+ {databaseError ?
:
}
+
+ {databaseError ||
+ `${databaseResult?.action === 'backup' ? 'Verified backup exported' : 'Database restored'} to ${databaseResult?.path} (${databaseResult?.bytes} bytes).`}
+
+
+ )}
{report?.services.map((service) => {
const running = service.loaded && service.state === 'running'
diff --git a/apps/web/src/test/fixtures.ts b/apps/web/src/test/fixtures.ts
index 17e55065..116d3baa 100644
--- a/apps/web/src/test/fixtures.ts
+++ b/apps/web/src/test/fixtures.ts
@@ -1,4 +1,5 @@
import { demoEvidence } from '../demo'
+import webPackage from '../../package.json'
import type {
AnswerResponse,
AuditEvent,
@@ -161,7 +162,7 @@ export const desktopSettings: DesktopSettings = {
}
export const desktopInfo: DesktopInfo = {
- desktop_version: '0.11.2',
+ desktop_version: webPackage.version,
backend_origin: 'http://127.0.0.1:7331',
autostart_enabled: false,
platform: 'macos',
@@ -177,11 +178,11 @@ export const desktopAuditEvents: AuditEvent[] = [
]
export const desktopUpdate: DesktopUpdate = {
- current_version: '0.11.2',
+ current_version: webPackage.version,
available_version: '9.9.9',
release_date: '2026-07-30T00:00:00Z',
release_notes: 'Fixes and improvements.',
- changelog: '0.11.2 release notes.',
+ changelog: `${webPackage.version} release notes.`,
github_url: 'https://github.com/0xPlayerOne/cortana',
phase: 'available',
downloaded_bytes: 0,
diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts
index f78f3212..c96178fa 100644
--- a/apps/web/src/types.ts
+++ b/apps/web/src/types.ts
@@ -537,6 +537,13 @@ export type DesktopServiceReport = {
}>
}
+export type DesktopDatabaseActionResult = {
+ action: 'backup' | 'restore'
+ path: string
+ bytes: number
+ detail: string
+}
+
export type DesktopSchedule = {
sync_interval_seconds: number
backup_interval_seconds: number
diff --git a/docs/desktop-ux-audit.md b/docs/desktop-ux-audit.md
index b3115a64..54567c89 100644
--- a/docs/desktop-ux-audit.md
+++ b/docs/desktop-ux-audit.md
@@ -64,25 +64,27 @@ is not part of a visual/UI change.
## Evidence limits
-- The v0.29.12 promotion (`3836df7`) is merged on both `main` and `staging` and tagged
- `v0.29.12`. Release-assets run `30965178674` completed successfully and the local verifier passed:
+- The v0.29.13 promotion (`2eb1f45`) is merged on both `main` and `staging` and tagged
+ `v0.29.13`. Release-assets run `30967558201` completed successfully and the local verifier passed:
all 18 assets, core
checksums, the macOS sidecar/resources bundle, all six minisign updater signatures, the updater
manifest, and the Linux runner `--version` check against the release tag are verified.
- The installed CLI `/Users/amf/.local/bin/cortana` and the installed packaged Desktop app
- `/Applications/Cortana.app` both report `cortana 0.29.12`; the CLI passes `cortana doctor`, the
+ `/Applications/Cortana.app` both report `cortana 0.29.13`; the CLI passes `cortana doctor`, the
offline evaluation checks, and the disposable desktop control-plane drill. The packaged app
passes `codesign --verify --deep --strict`, but remains ad-hoc signed (`TeamIdentifier` is
unset) and is rejected by `spctl --assess`. Developer ID signing/notarization therefore remains
a release blocker.
-- The v0.29.11-line local validation passed the confirmed counts: `cargo test --lib` 260 passed;
- `bun test --max-concurrency=1` 250 passed; `uv run pytest -q` 149 passed; `bun run typecheck`
- passed. These are per-suite local figures on the v0.29.11 line, not a deduplicated aggregate and
- not the promotion-run tallies previously reported for v0.29.8.
-- The v0.29.11 line includes Desktop changes, so the v0.29.8 "no Desktop changes, matrix skipped"
- note does not apply. No packaged Desktop build/test matrix results are claimed for v0.29.11
- beyond the control-plane drill evidence above; the 19/11 focused readiness figures were
- specific to the v0.29.8 readiness-hardening patch and are not re-asserted.
+- The prepared-sidecar v0.29.12 Desktop native suite passed 123 tests. The previously confirmed
+ v0.29.11-line figures remain: `cargo test --lib` 260 passed; `bun test --max-concurrency=1`
+ 250 passed; `uv run pytest -q` 149 passed; and `bun run typecheck` passed. These are prior
+ per-suite figures, not a deduplicated aggregate or a fresh claim about this checkout's full
+ Web suite; the refreshed `uv.lock` itself passes `uv lock --check`.
+- The v0.29.12 line includes Desktop changes, so the v0.29.8 "no Desktop changes, matrix skipped"
+ note does not apply. The 123-test native suite is a headless Tauri IPC check after preparing the
+ release sidecar; it does not claim packaged GUI, browser, OS-service, or signed-updater behavior.
+ The 19/11 focused readiness figures were specific to the v0.29.8 readiness-hardening patch and
+ are not re-asserted.
- Full `cortana readiness` is a read-only, comprehensive check that includes roughly 1 GB of
SQLite integrity and backup scanning. In the observed run, the database integrity scan took
about 130 seconds and the backup scan about 80 seconds. `GET /healthz` is only an
diff --git a/uv.lock b/uv.lock
index 8ecb3631..5d4d0f8e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -652,7 +652,7 @@ wheels = [
[[package]]
name = "cortana-brain"
-version = "0.27.0"
+version = "0.29.13"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
@@ -856,7 +856,7 @@ name = "cuda-bindings"
version = "13.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cuda-pathfinder", marker = "python_full_version < '3.15' and sys_platform != 'win32'" },
+ { name = "cuda-pathfinder" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" },
@@ -889,43 +889,43 @@ wheels = [
[package.optional-dependencies]
cublas = [
- { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
- { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cudart = [
- { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cufft = [
- { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
- { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cufile = [
- { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cupti = [
- { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
curand = [
- { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cusolver = [
- { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
- { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
- { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
- { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
cusparse = [
- { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
- { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
nvjitlink = [
- { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
nvrtc = [
- { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
nvtx = [
- { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
]
[[package]]
@@ -2696,14 +2696,14 @@ name = "mlx-lm"
version = "0.31.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "jinja2", marker = "sys_platform != 'win32'" },
+ { name = "jinja2" },
{ name = "mlx", marker = "sys_platform == 'darwin'" },
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform != 'win32'" },
- { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and sys_platform != 'win32'" },
- { name = "protobuf", marker = "sys_platform != 'win32'" },
- { name = "pyyaml", marker = "sys_platform != 'win32'" },
- { name = "sentencepiece", marker = "sys_platform != 'win32'" },
- { name = "transformers", marker = "sys_platform != 'win32'" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
+ { name = "protobuf" },
+ { name = "pyyaml" },
+ { name = "sentencepiece" },
+ { name = "transformers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/94/9a38d6b0c6fcca995b9136c94eb7da1e9c5165652edf228b96b29960fa7a/mlx_lm-0.31.3.tar.gz", hash = "sha256:61eb0e3ba09444f77f874aff295401d7ccd20b39495cbbce0c782a15474ce733", size = 304318, upload-time = "2026-04-22T07:37:27.922Z" }
wheels = [
@@ -3081,7 +3081,7 @@ name = "nvidia-cublas"
version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'win32'" },
+ { name = "nvidia-cuda-nvrtc" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
@@ -3120,7 +3120,7 @@ name = "nvidia-cudnn-cu13"
version = "9.20.0.48"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-cublas", marker = "sys_platform != 'win32'" },
+ { name = "nvidia-cublas" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
@@ -3132,7 +3132,7 @@ name = "nvidia-cufft"
version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" },
+ { name = "nvidia-nvjitlink" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
@@ -3162,9 +3162,9 @@ name = "nvidia-cusolver"
version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-cublas", marker = "sys_platform != 'win32'" },
- { name = "nvidia-cusparse", marker = "sys_platform != 'win32'" },
- { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" },
+ { name = "nvidia-cublas" },
+ { name = "nvidia-cusparse" },
+ { name = "nvidia-nvjitlink" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
@@ -3176,7 +3176,7 @@ name = "nvidia-cusparse"
version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" },
+ { name = "nvidia-nvjitlink" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
@@ -4986,7 +4986,7 @@ resolution-markers = [
"python_full_version < '3.12' and sys_platform != 'win32'",
]
dependencies = [
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
@@ -5067,7 +5067,7 @@ resolution-markers = [
"python_full_version == '3.12.*' and sys_platform != 'win32'",
]
dependencies = [
- { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
wheels = [
@@ -5118,8 +5118,8 @@ name = "secretstorage"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cryptography", marker = "sys_platform != 'win32'" },
- { name = "jeepney", marker = "sys_platform != 'win32'" },
+ { name = "cryptography" },
+ { name = "jeepney" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
wheels = [