Skip to content

feat(sync): per-profile sync mode toggle (Phase 1.f.desktop.3) - #194

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/1-f-desktop-3-sync-mode-toggle
May 31, 2026
Merged

feat(sync): per-profile sync mode toggle (Phase 1.f.desktop.3)#194
InstaZDLL merged 2 commits into
mainfrom
feat/1-f-desktop-3-sync-mode-toggle

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented May 31, 2026

Copy link
Copy Markdown
Owner

Adds the user-facing "off-switch" the enqueue hooks #192 shipped need to be controllable. Per-profile sync mode persisted in `profile_setting['sync.mode']`; the existing `sync::hooks` gate now short-circuits when mode = `Local` even with a JWT configured.

Tighter scope than the original RFC-001 spec — see "Why no Server-connected mode?" below for the rationale.

Backend (~250 LOC)

  • `crate::sync::mode` — `SyncMode` enum (`Local`, `Hybrid`), read/write helpers around `profile_setting`, 5 unit tests covering fresh-profile default, round-trip both directions, unknown-storage-value fallback, and const `as_str`/`from_storage` symmetry.
  • `sync::hooks::enqueue_op` gate: if no JWT skip; if `SyncMode::Local` skip; otherwise `lamport::next` + `queue::enqueue`. Fresh profile defaults to `Hybrid` so the post-sign-in flow Just Works.
  • New Tauri commands `sync_get_mode` and `sync_set_mode` (canonical string round-trip, rejects unknown modes).
  • `sync_get_queue_state` extended with a `mode` field so the Settings card renders both queue stats AND the active mode in one round-trip.

Frontend (~120 LOC)

  • `src/lib/tauri/serverAuth.ts` wrapper: `SyncMode` type + `syncGetMode` + `syncSetMode`.
  • `ServerAccountCard` gains a radio under the JWT section, gated on `signedIn && mode` loaded so we don't flash an empty group during hydration. `Promise.all([serverGetStatus, syncGetMode])` on initial load batches the two reads. `syncGetMode` failure during initial hydration is caught and the radio simply stays hidden rather than blowing up the whole card (race against profile-switch).

i18n

  • `settings.serverAccount.{modeLabel, modes.hybrid.{label, description}, modes.local.{label, description}}` propagated to all 17 locales per the CLAUDE.md convention.

Why no `Server-connected` mode?

RFC-001 listed a third "thin-client" mode where reads come from HTTP instead of local SQLite. Deferred — `waveflow-web` already covers the thin-client use case, and routing desktop reads through HTTP forfeits the value the local audio engine + file scanner provide. The `SyncMode` enum is intentionally open-shaped (`from_storage` falls back to `Hybrid` for any unknown value) so a future `ServerOnly` variant lands without touching the persistence + gate logic.

Test plan

Summary by CodeRabbit

  • Nouvelles Fonctionnalités

    • Mode de synchronisation configurable : choix entre Hybride (par défaut) et Local depuis la carte de compte serveur (groupe de radios, état de progression).
    • API cliente exposée pour lire/écrire le mode de sync, utilisé par l’interface.
    • Traductions ajoutées pour l’option de mode dans 16 langues.
  • Corrections

    • La vue diagnostic reçoit désormais un état de mode même si le profil n’est pas disponible, évitant des valeurs manquantes.

Adds the user-facing "off-switch" the enqueue hooks #192 shipped need
to be controllable. Per-profile sync mode persisted in
profile_setting['sync.mode']; the existing sync::hooks gate now
short-circuits when mode = Local even with a JWT configured.

## Backend (~250 LOC)

- crate::sync::mode — SyncMode enum (Local, Hybrid), read/write
  helpers around profile_setting, 5 unit tests covering fresh-profile
  default, round-trip both directions, unknown-storage-value fallback,
  and const as_str/from_storage symmetry.
- sync::hooks::enqueue_op gate: if no JWT skip; if SyncMode::Local
  skip; otherwise lamport::next + queue::enqueue. Fresh profile
  defaults to Hybrid so the post-sign-in flow Just Works.
- New Tauri commands sync_get_mode and sync_set_mode (canonical
  string round-trip, rejects unknown modes with 400-style error).
- sync_get_queue_state extended with a mode field so the Settings
  card renders both queue stats AND the active mode in one
  round-trip.

## Frontend (~120 LOC)

- src/lib/tauri/serverAuth.ts wrapper: SyncMode type + syncGetMode +
  syncSetMode.
- ServerAccountCard gains a radio under the JWT section, gated on
  signedIn && mode loaded so we don't flash an empty group during
  hydration. Promise.all([serverGetStatus, syncGetMode]) on initial
  load batches the two reads.
- syncGetMode failure during initial hydration is caught and the
  radio simply stays hidden rather than blowing up the whole card
  (race against profile-switch).

## i18n

- settings.serverAccount.{modeLabel, modes.hybrid.label,
  modes.hybrid.description, modes.local.label, modes.local.description}
  propagated to all 17 locales per the CLAUDE.md convention.

## Why no Server-connected mode?

RFC-001 listed a third "thin-client" mode where reads come from
HTTP instead of local SQLite. Deferred — waveflow-web already
covers the thin-client use case, and routing desktop reads through
HTTP forfeits the value the local audio engine + file scanner
provide. The SyncMode enum is intentionally open-shaped so a future
ServerOnly variant lands without touching the persistence + gate
logic.

## Test plan

- cargo test -p waveflow --lib sync::mode (5/5 green)
- cargo clippy -p waveflow --all-targets -- -D warnings
- cargo fmt -p waveflow --check
- bun run typecheck
- bun run lint
- Manual smoke (requires #190 + waveflow-web #18 deployed):
  - Sign in to a profile → Settings → Compte serveur shows radio
    defaulted to "Hybrid"
  - Create a playlist → sync_get_queue_state shows pending_count: 1
  - Flip to Local → pending_count stays 1, no new ops enqueue
  - Update playlist name → pending_count stays 1 (queue gated by mode)
  - Flip back to Hybrid → update playlist again → pending_count: 2

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

PR ajoute un système de mode de synchronisation (Hybrid / Local) par profil : enum et persistence SQLite, nouvelles commandes Tauri pour lire/écrire le mode, court-circuit de la queue backend en mode Local, wrapper TypeScript IPC, sélecteur radio dans Settings et traductions pour plusieurs locales.

Changes

Sync Mode Configuration & UI

Layer / File(s) Summary
Sync mode schema & persistence
src-tauri/crates/app/src/sync/mode.rs
Enum SyncMode (Local, Hybrid) avec conversion texte, read/write async en SQLite profile_setting("sync.mode"), fallback Hybrid par défaut, tests unitaires couvrant round-trips et cas d'erreur.
Tauri command handlers
src-tauri/crates/app/src/commands/sync.rs
Ajout de sync_get_mode() et sync_set_mode(req) avec validation (trim + map "local"/"hybrid"), persistance via mode::write(), et ajout du champ mode dans SyncQueueState (fallback Hybrid si pool indisponible).
Backend wiring & queue integration
src-tauri/crates/app/src/lib.rs, src-tauri/crates/app/src/sync/hooks.rs, src-tauri/crates/app/src/sync/mod.rs
Enregistrement des nouvelles commandes dans invoke_handler, exposition du module sync::mode, et court-circuit dans enqueue_op_inner quand SyncMode::Local ou pas de JWT (avant Lamport/enqueue).
Frontend IPC wrapper
src/lib/tauri/serverAuth.ts
Export SyncMode TypeScript et helpers syncGetMode() / syncSetMode() qui appellent via IPC sync_get_mode / sync_set_mode.
Settings UI & localization
src/components/views/settings/ServerAccountCard.tsx, src/i18n/locales/*.json
ServerAccountCard charge/affiche le mode avec radios (hybrid/local), gère mode et modeBusy, handleSetMode pour persister le choix, et ajouts i18n pour le label et descriptions du mode dans plusieurs fichiers de locales.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

size: m

Poem

🌐 Choisir Hybrid ou Local, l'utilisateur décide,
SQLite conserve, Tauri guide,
React montre le basculeur en douceur,
La queue se tait quand le profil veut la paix,
Seize langues murmurent ce petit bonheur.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit Conventional Commits avec un scope kebab-case valide et décrit clairement le changement principal : un toggle de mode de sync par profil.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed La description couvre tous les éléments essentiels : résumé clair, justification (RFC-001 reporté), test plan détaillé, changements backend/frontend documentés et checklist complétée.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1-f-desktop-3-sync-mode-toggle

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) type: feat New feature size: xl > 500 lines labels May 31, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/app/src/sync/mode.rs`:
- Around line 110-113: La requête UPSERT sur la table profile_setting n’inclut
pas value_type dans le SET de ON CONFLICT; modifie la clause ON CONFLICT de la
chaîne SQL (celle qui commence par "INSERT INTO profile_setting ... ON
CONFLICT(key) DO UPDATE") pour mettre à jour aussi value_type en plus de value
et updated_at (par exemple ajouter value_type = excluded.value_type dans la
liste des colonnes mises à jour) afin de garantir que le type enregistré reste
cohérent avec la valeur.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f64e67dd-7d0e-443b-9648-d301f1201663

📥 Commits

Reviewing files that changed from the base of the PR and between 3d13a60 and 8192ffa.

📒 Files selected for processing (24)
  • src-tauri/crates/app/src/commands/sync.rs
  • src-tauri/crates/app/src/lib.rs
  • src-tauri/crates/app/src/sync/hooks.rs
  • src-tauri/crates/app/src/sync/mod.rs
  • src-tauri/crates/app/src/sync/mode.rs
  • src/components/views/settings/ServerAccountCard.tsx
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json
  • src/lib/tauri/serverAuth.ts

Comment thread src-tauri/crates/app/src/sync/mode.rs Outdated
@coderabbitai on PR #194 flagged that the mode::write UPSERT didn't
refresh value_type on conflict. For sync.mode specifically value_type
never drifts (mode::write is the only writer and always inserts
'string'), but the fix is four characters and closes the class of
bugs where a hypothetical future writer puts a wrong type in the row
that this UPSERT would then silently preserve. Cheap defence in
depth — same shape every future UPSERT against profile_setting
should adopt.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL
InstaZDLL merged commit 1e274fb into main May 31, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/1-f-desktop-3-sync-mode-toggle branch May 31, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant