Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
46647d2
docs: high-priority improvements design spec (4 items)
cortexuvula Jul 8, 2026
2cc94ba
docs: implementation plans for 4 high-priority improvements
cortexuvula Jul 8, 2026
72dff82
docs: medium-priority improvements design spec (items #5-#8)
cortexuvula Jul 8, 2026
6ec4a09
docs: implementation plans for 4 medium-priority improvements
cortexuvula Jul 8, 2026
1574764
docs: low-priority improvements spec + plans (items #9-#10)
cortexuvula Jul 8, 2026
0cf57a6
perf: only poll condition chips when sync is enabled
cortexuvula Jul 8, 2026
49afcc9
feat(ui): keyboard shortcuts — Space for record/stop, Cmd+Enter for SOAP
cortexuvula Jul 8, 2026
d115f2c
refactor: extract join_err helper to eliminate spawn_blocking boilerp…
cortexuvula Jul 8, 2026
c9a0038
test(db): condition chip sync round-trip integration test
cortexuvula Jul 8, 2026
6e4d6a7
chore: triage dependabot backlog — close failing PRs + add ignore rules
cortexuvula Jul 8, 2026
03900ba
refactor: split Sharing.svelte into SharingModes + ConditionChipSync
cortexuvula Jul 8, 2026
0a7177c
feat(ui): toast when condition chips change from another machine
cortexuvula Jul 8, 2026
2029310
fix(release): consolidated latest.json after all platform assets
cortexuvula Jul 8, 2026
7be4288
feat(sharing): SSE endpoint for condition chip change notifications
cortexuvula Jul 8, 2026
e08dc94
feat(sync): SSE client + subscribe command for realtime chip sync
cortexuvula Jul 8, 2026
8ed56ea
feat(frontend): listen for SSE chip change events for realtime sync
cortexuvula Jul 8, 2026
5d01a62
feat(db): m012 migration + repo methods for async encryption
cortexuvula Jul 8, 2026
6a8064f
perf(audio): stop_recording returns immediately via background encryp…
cortexuvula Jul 8, 2026
f6308f4
feat(audio): startup sweep for pending encryption (crash recovery)
cortexuvula Jul 8, 2026
1543f2a
style: cargo fmt after async encryption changes
cortexuvula Jul 8, 2026
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
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ updates:
update-types:
- "minor"
- "patch"
ignore:
# Major bumps requiring dedicated migration effort (multi-crate, API changes)
- dependency-name: "rand"
update-types: ["version-update:semver-major"]
- dependency-name: "zip"
update-types: ["version-update:semver-major"]
- dependency-name: "rubato"
update-types: ["version-update:semver-major"]
- dependency-name: "sha2"
update-types: ["version-update:semver-major"]
- dependency-name: "rodio"
update-types: ["version-update:semver-major"]

- package-ecosystem: "github-actions"
directory: "/"
Expand Down
89 changes: 88 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
# matches FerriScribe version tags (vX.Y.Z); leaves whisper-server-* and
# other non-version tags untouched. Runs once after all platform builds.
prune:
needs: release
needs: [release, manifest]
runs-on: ubuntu-24.04
permissions:
contents: write
Expand All @@ -120,3 +120,90 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/prune-old-releases.mjs

# Generate a consolidated latest.json AFTER all platform builds complete.
# Each platform job auto-generates its own partial latest.json via
# tauri-action; this job overwrites it with a manifest that includes ALL
# platforms, eliminating the updater race condition where clients see a
# new version before their platform binary is uploaded.
manifest:
needs: release
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Generate consolidated latest.json
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
echo "Generating consolidated latest.json for $TAG"

# Get release metadata
PUB_DATE=$(gh release view "$TAG" --json createdAt -q .createdAt)
NOTES=$(gh release view "$TAG" --json body -q .body)

# List all assets
ASSETS=$(gh release view "$TAG" --json assets -q '.assets[].name')

# Generate latest.json using Node
node << 'SCRIPT'
const { execSync } = require('child_process');
const fs = require('fs');

const tag = process.env.GITHUB_REF.replace('refs/tags/', '');
const version = tag.replace(/^v/, '');
const repo = 'cortexuvula/rustMedicalAssistant';
const baseUrl = `https://github.com/${repo}/releases/download/${tag}`;
const pubDate = execSync(`gh release view ${tag} --json createdAt -q .createdAt`).toString().trim();
const notes = execSync(`gh release view ${tag} --json body -q .body`).toString().trim();

const assetsJson = JSON.parse(execSync(`gh release view ${tag} --json assets`).toString());
const assetNames = assetsJson.assets.map(a => a.name);

const platforms = {};

for (const name of assetNames) {
if (!name.endsWith('.sig')) continue;
const baseAsset = name.replace(/\.sig$/, '');
if (!assetNames.includes(baseAsset)) continue;

// Read the signature content
const signature = execSync(`gh release download ${tag} --pattern "${name}" --output - 2>/dev/null`).toString().trim();

// Map filename to Tauri platform key
let platform = null;
if (name.includes('.deb') || name.includes('.rpm')) platform = 'linux-x86_64';
else if (name.includes('.msi') || name.includes('-setup.exe')) platform = 'windows-x86_64';
else if (name.includes('aarch64.app')) platform = 'darwin-aarch64';
else if (name.includes('x86_64.app')) platform = 'darwin-x86_64';
else continue;

// Only keep the NSIS updater format for Windows (not MSI)
if (name.includes('.msi') && assetNames.some(n => n.includes('-setup.exe'))) continue;

platforms[platform] = {
signature,
url: `${baseUrl}/${baseAsset}`,
};
}

const manifest = {
version,
notes,
pub_date: pubDate,
platforms,
};

console.log('Generated manifest:', JSON.stringify(manifest, null, 2));
fs.writeFileSync('latest.json', JSON.stringify(manifest, null, 2));
SCRIPT

- name: Upload consolidated latest.json
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
gh release upload "$TAG" latest.json --clobber
echo "Consolidated latest.json uploaded to release $TAG"
25 changes: 24 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions crates/db/src/migrations/m012_encryption_pending.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use rusqlite::Connection;

use crate::DbResult;

/// Add `encryption_pending` column to `recordings`.
///
/// Tracks whether a recording's WAV file is still being encrypted by a
/// background task spawned from `stop_recording`. A startup sweep reads
/// rows left at `1` (the app crashed or was hard-quit mid-encryption) and
/// encrypts them so no plaintext audio remains at rest.
pub fn up(conn: &Connection) -> DbResult<()> {
conn.execute_batch(
"ALTER TABLE recordings ADD COLUMN encryption_pending INTEGER NOT NULL DEFAULT 0;",
)?;
Ok(())
}
6 changes: 6 additions & 0 deletions crates/db/src/migrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod m008_peer_discussion;
pub mod m009_soft_delete;
pub mod m010_condition_chips;
pub mod m011_condition_chip_order;
pub mod m012_encryption_pending;

use rusqlite::Connection;

Expand Down Expand Up @@ -94,6 +95,11 @@ pub fn all_migrations() -> &'static [Migration] {
name: "condition_chip_order",
up: m011_condition_chip_order::up,
},
Migration {
version: 12,
name: "encryption_pending",
up: m012_encryption_pending::up,
},
]
}

Expand Down
39 changes: 39 additions & 0 deletions crates/db/src/recordings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,45 @@ impl RecordingsRepo {
Ok(updated as u32)
}

/// Mark a recording's background encryption as complete (clears the
/// `encryption_pending` flag set by `stop_recording`).
///
/// Called from the `spawn_blocking` task that encrypts the WAV file, on
/// both success and failure — the flag is "encryption attempt finished",
/// not "encryption succeeded". On failure the recording is left as
/// plaintext at rest, which the reader (`open_recording_wav`) handles
/// transparently.
pub fn set_encryption_done(conn: &Connection, id: &Uuid) -> DbResult<()> {
conn.execute(
"UPDATE recordings SET encryption_pending = 0 WHERE id = ?1",
[&id.to_string()],
)?;
Ok(())
}

/// Return the `(id, audio_path)` of every recording still flagged
/// `encryption_pending = 1`.
///
/// Used by the startup sweep to encrypt WAVs left plaintext by a crash
/// or hard-quit mid-encryption. Rows with a missing/unparseable id or
/// path are dropped (they can't be encrypted by name).
pub fn list_encryption_pending(conn: &Connection) -> DbResult<Vec<(Uuid, PathBuf)>> {
let mut stmt =
conn.prepare("SELECT id, audio_path FROM recordings WHERE encryption_pending = 1")?;
let rows = stmt
.query_map([], |row| {
let id_str: String = row.get(0)?;
let path: String = row.get(1)?;
Ok((
Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
PathBuf::from(path),
))
})?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}

/// Fetch multiple recordings by ID in a single query.
///
/// Order is not guaranteed; sort the result on the caller side if needed.
Expand Down
Loading
Loading