Files
thoughtsync/desktop/src-tauri/src/commands/local.rs
T
Bryan Van Deusen f992439588
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m50s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m19s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 7m59s
version: every surface can say which build it is, and two of them were lying
Note 3127 §5 removed version tags, so an artifact's self-report is now the only
answer to "which build is this?" — and nothing exists to contradict it when it
is wrong. Three surfaces gain a dim build line: the foot of the web rail, the
login screen, and the foot of Sync on Android.

The login screen because "I can't sign in" is a bug report like any other, and
requiring an account to read a build number withholds it from exactly the people
who can't get past that page. `/api/config` is already public.

Two of the values it was going to show were wrong, which is the part worth
knowing about.

The DESKTOP reported `env!("CARGO_PKG_VERSION")` from `config_get` and from the
startup log. `cargo tauri build --config '{"version": ...}'` overrides
tauri.conf.json, not Cargo's own metadata — so both read the literal `0.2.0` in
Cargo.toml, on every build ever shipped. They now read a display version baked in
by the lane through `option_env!`, hoisted to the crate root because two readers
of one fact is how this repo keeps producing 2181-2183. Not the ordering key
either: `1.0.<minutes>` is the opaque value Tauri's updater compares and must
never be shown to a person, and `update.rs` still reads it because a comparator
is exactly what it is (rule 149).

The SERVER fell back to `__version__` when APP_VERSION was absent, so a server
run from a checkout reported `0.2.0` — a real-looking version naming no build
anybody could obtain. `__init__.py` already asserted the honest answer was
"APP_VERSION being missing, which app.py already handles"; it did not, and a
comment claiming a behaviour two files away is how that stayed true-sounding.
Now an explicit "unknown", with the packaging version left where "unknown" is
not a legal value.

Android reads the INSTALLED package's versionName rather than BuildConfig, so it
reports what is actually on the phone.

Everything renders "unknown" rather than blank when it cannot say. A blank looks
like a layout bug; a plausible default cannot be caught by anything.

build.rs gets `rerun-if-env-changed` for the baked value: cargo does not track an
`option_env!` variable on its own, and the desktop lane having no cache today is
what makes that easy to forget the day one is added.
2026-08-29 23:07:29 -04:00

262 lines
9.1 KiB
Rust

//! Tauri command surface for the local store — the offline mirror of the REST API
//! the frontend's repository seam calls. Each command locks the shared connection and
//! delegates to `store`; errors are stringified for the JS boundary. adapters/local.ts
//! (M10.5) invokes these.
use serde_json::Value;
use tauri::State;
use thoughtsync_core::local::models::*;
use thoughtsync_core::local::retention;
use thoughtsync_core::local::store;
use thoughtsync_core::local::Db;
use thoughtsync_core::sync::state;
// A macro would hide the (very regular) locking; kept explicit so each command reads
// as an obvious lock -> delegate -> stringify.
#[tauri::command]
pub fn config_get(db: State<'_, Db>) -> PublicConfig {
// What the Trash view counts down against: the linked server's window if we know
// it, else this device's own. Reading it here rather than hard-coding the offline
// default is what keeps the deadline on screen equal to the one that will actually
// be enforced. A store error falls back to the default rather than failing the
// call — the app must still boot.
let fallback = retention::LOCAL_RETENTION_DAYS;
let retention_days = match db.0.lock() {
Ok(conn) => state::effective_retention_days(&conn, fallback).unwrap_or(fallback),
Err(_) => fallback,
};
// Offline defaults: no signups, no server-side URL unfurling (needs network).
PublicConfig {
site_name: "ThoughtSync".to_string(),
allow_registration: false,
// The build a person reads, baked at compile time — see crate::display_version
// for why this is neither CARGO_PKG_VERSION nor the updater's ordering key.
version: crate::display_version().to_string(),
enable_url_unfurl: false,
trash_retention_days: retention_days.max(0) as u32,
}
}
#[tauri::command]
pub fn auth_me() -> User {
// The single synthetic local user, so the auth-gated router resolves with no login.
User {
id: "local".to_string(),
email: "local@thoughtsync.app".to_string(),
display_name: "You".to_string(),
email_verified: true,
is_admin: false,
}
}
#[tauri::command]
pub fn notes_list(query: ListQuery, db: State<'_, Db>) -> Result<Vec<Note>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::list_notes(&conn, &query).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_get(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::get_note(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_create(input: NoteCreateInput, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::create_note(&conn, &input).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_update(id: String, changes: Value, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::update_note(&conn, &id, &changes).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_complete_reminder(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::complete_reminder(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_snooze_reminder(id: String, minutes: i64, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::snooze_reminder(&conn, &id, minutes).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_set_labels(
id: String,
label_ids: Vec<String>,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::set_labels(&conn, &id, &label_ids).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_add_item(id: String, text: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::add_item(&conn, &id, &text).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_update_item(
id: String,
item_id: String,
changes: Value,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::update_item(&conn, &id, &item_id, &changes).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_item(id: String, item_id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_item(&conn, &id, &item_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_attachment(
id: String,
att_id: String,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_attachment(&conn, &id, &att_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_preview(
id: String,
preview_id: String,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_preview(&conn, &id, &preview_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_reorder(ordered_ids: Vec<String>, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::reorder(&conn, &ordered_ids).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_trash(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::trash(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_restore(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::restore(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_forever(id: String, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_forever(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_revisions(id: String, db: State<'_, Db>) -> Result<Vec<NoteRevision>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::revisions(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_restore_revision(
id: String,
rev_id: String,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::restore_revision(&conn, &id, &rev_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_reminders(db: State<'_, Db>) -> Result<Vec<Note>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::reminders(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_titles(db: State<'_, Db>) -> Result<Vec<TitleEntry>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::titles(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_list(db: State<'_, Db>) -> Result<Vec<Label>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::list_labels(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_create(name: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::create_label(&conn, &name).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_rename(id: String, name: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::rename_label(&conn, &id, &name).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_set_color(id: String, color: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::set_label_color(&conn, &id, &color).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_remove(id: String, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::remove_label(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_merge(source_id: String, into: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::merge_labels(&conn, &source_id, &into).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_list(db: State<'_, Db>) -> Result<Vec<SavedFilter>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::list_saved_filters(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_create(
name: String,
params: Value,
db: State<'_, Db>,
) -> Result<SavedFilter, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::create_saved_filter(&conn, &name, &params).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_remove(id: String, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::remove_saved_filter(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_rename(
id: String,
name: String,
db: State<'_, Db>,
) -> Result<SavedFilter, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::rename_saved_filter(&conn, &id, &name).map_err(|e| e.to_string())
}