45fec2310eab4db75a4d763cde76386d02f95041
59
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
45fec2310e |
fix(ptt): disable missed-key-up watchdog on P0 (DEC-031, supersedes DEC-028)
The watchdog spawned by ChanoraSession::start_audio cleared ptt_held after 30 s of continuous PTT key-down. That was correct for the 'OS lost the key-up event' failure mode the original SAD-079 / DEC-028 was designed to catch, but it was the wrong shape for real human speech: anyone holding the bound key for a long answer got cut off mid-sentence. For P0: - Comment out the spawn site in ChanoraSession::start_audio with the rationale + the P1 redesign options under consideration (raised ceiling / OS key-state polling / RMS-silence fallback). - Leave the MissedKeyUpWatchdog Rust type, its spawn / spawn_on_signal entry points, and all unit tests in chanora_audio::ptt unchanged so P1 can re-enable with the chosen detection strategy without re-implementing anything. Spec: new DEC-031 in product-decision-register.md supersedes DEC-028 for the v1 ship. DEC-028 stays in the register as historical context. The §7 open-decisions log + §8 change history get matching 0.9.12 rows. Note: Mumble and TeamSpeak ship without a comparable watchdog — the 30 s ceiling was stricter than industry baseline. The underlying protection (OS-level key-up loss) is still worth solving, just not with a fixed timeout. cargo test --workspace --lib: 80 passed / 0 failed / 1 ignored (unchanged; the watchdog unit tests still run because the type itself is unchanged). docs validator: clean (pre-existing 35-filename warning only). |
||
|
|
6d4975bd6e |
fix(ptt,ui): Continuous mode no longer self-disables after 30 s; rename PTT label to Mic
Issue 1: in Continuous transmit mode the talk indicator turned
gray-out / mic disabled after ~30 s and could only be revived by
toggling mic mute. Root cause: SAD-079 MissedKeyUpWatchdog
subscribed to AudioTransmitGate.transmit_active and force-cleared
it after 30 s of true. In PTT mode this is correct (stuck key =
bug). In Continuous mode transmit_active is *supposed* to stay
true indefinitely; the watchdog assumption doesn't hold.
Fix: the watchdog now subscribes to a new ptt_held watch on the
TransmitModeSelector (the raw key-state input, not the resolved
gate). In Continuous mode ptt_held is never set true, so the
watchdog never fires. In PTT mode it still fires on a stuck
key-down as before. The session owns the watchdog (was on the
engine) so it survives engine restarts; it's spawned lazily on the
first start_audio.
MissedKeyUpWatchdog gains spawn_on_signal(rx, on_timeout, timeout)
alongside the existing spawn(gate, timeout) — old shape preserved
for backwards compat. run_watchdog generalised to take any
watch::Receiver<bool> + Box<dyn Fn() + Send + Sync>.
Two new tests:
- watchdog_on_signal_does_not_fire_when_ptt_held_stays_false
(the Continuous-mode regression test)
- watchdog_on_signal_fires_when_signal_stays_true
(the stuck-key case still fires)
Issue 2: the Voice Bar stats line said 'PTT on/off' even when the
user was in Continuous mode where no PTT key is involved. Renamed
to 'Mic on/off' (mode-neutral) and l10n-ised the on/off literal:
- en: 'Mic on' / 'Mic off'
- zh: '麦克风 开启' / '麦克风 关闭'
cargo test --workspace --lib: 80 passed / 0 failed / 1 ignored
(was 78, +2 watchdog tests).
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
|
||
|
|
21945979a3 |
test(audio,ptt): comprehensive Windows P0 unit-test suite (L0-L11)
Layered test coverage for the Windows PTT subsystem ahead of the
v1.0.0-rc.8 official release sign-off.
L0 (refactor)
- Extract three pure-logic dispatchers from the existing WndProc /
LowLevelKeyboardProc / LowLevelMouseProc bodies in
crates/chanora_audio/src/ptt_backends/windows.rs:
dispatch_raw_input(ctx, &RAWINPUT)
dispatch_hook_keyboard(ctx, wparam, &KBDLLHOOKSTRUCT)
dispatch_hook_mouse(ctx, wparam, &MSLLHOOKSTRUCT)
Each takes a small Context (AtomicBinding + AudioTransmitGate +
flags) and is callable without spinning up any Win32 plumbing.
The real Win32 procs unchanged structurally; they unpack lparam
and forward to the dispatchers. AtomicBinding / RawInputContext
/ HookContext / resolve_binding are now pub(crate) so the
in-file test module can drive them.
L1 — windows_keymap full-table sweep (+13 tests)
Every key_label_to_vk arm, all A-Z + a-z, all 0-9, F1-F20,
navigation, modifiers, OEM punctuation, numpad. Exhaustive
mouse_label_to_button cases including the 0x08 / 0x10 /
unknown-bitmask fallbacks.
L2 — AtomicBinding lock-free correctness
store/read round-trip, clear(), Default = zeros, single-writer
/ single-reader concurrency, many-readers / single-writer.
L3 — resolve_binding dispatcher tests
All PttInputClass variants, well-known labels, unknown-label
fallback, mismatched class+label rejection, mouse bitmask
resolution.
L4 — Backend state-machine
Both WindowsRawInputBackend and WindowsHookBackend:
descriptor() pre-arm vs post-arm (L0Focused -> L2/L3), start()
with None binding rejection, rebind() in-place, stop()
clears + idempotent, stop() after stop() no-op.
L5 — dispatch_raw_input table
Keyboard match/non-match, key-down/key-up via Flags & 0x01,
no-binding short-circuit, mouse XBUTTON1/XBUTTON2 down/up
matching the bound button, unhandled HID type. RAWINPUT structs
built via mem::zeroed plus field-fill, owning the unsafe in
the test layer where it belongs.
L6 — dispatch_hook_keyboard + dispatch_hook_mouse
WM_KEYDOWN / WM_KEYUP / WM_SYSKEYDOWN / WM_SYSKEYUP for the
keyboard path, WM_XBUTTONDOWN / WM_XBUTTONUP for the mouse
path. Same shape as L5.
L7 — Privacy invariant (crates/chanora_audio/tests/ptt_privacy.rs)
New cross-platform integration test installs a custom
tracing_subscriber Layer that records every emitted event's
target + field names. Exercises the public PTT API plus (on
Windows) the backend factory. Asserts no field name in the
banned list (vk, scan_code, keysym, key_label, bound_key,
binding, platform_key, VKey, wVk, wScan, kbflags, mouseflags)
is ever emitted and every field belongs to the DEC-027
allow-list. Adds tracing-subscriber as a dev-dependency on
chanora_audio.
L8 — Full-chain integration in core/chanora_core/src/ptt.rs
Windows-only mod windows_full_chain_tests:
zero-tail full chain (synchronous)
default-tail full chain (200 ms wait then off)
mid-press rebind abandons in-flight press
L9/L10/L11 — tools/windows-smoke.cmd + tools/windows-smoke.md
Batch smoke script + operator doc. cargo build, flutter build,
artifact existence + size checks, headless launch with stderr
capture, bridge-initialised log assertion. Distinct exit codes
per failure step. Doc explains invocation + common failure
modes.
Verification (Linux)
- cargo check --workspace: clean.
- cargo test --workspace: 78 passed / 0 failed / 3 ignored.
76 cross-platform unit tests (unchanged) plus the new
ptt_privacy integration test plus one new ignored portal smoke
test.
The Windows-gated tests (~49 new) compile and run on the Korean
Windows 11 host where they belong; cross-compile from Linux is
not configured locally. The smoke script is the production
acceptance gate for rc.8 on Windows.
Deviations from the original plan are minor (single ignored
real-runtime test rather than per-platform attribute, L7 uses
public API rather than pub(crate) dispatchers, dispatchers live
inside windows.rs rather than a sibling module) and documented
in the subagent report.
|
||
|
|
7596f8a9dc |
fix(ui,voice): display 'Space' (not blank) in bind dialog; cut PTT poll latency
Issue 1: tapping Space in the PTT binding capture dialog set _captured to LogicalKeyboardKey.space.keyLabel which is ' ' (a single space character), rendering as a blank string in the 'Captured: ' display. Same problem for Enter, Tab, Backspace, etc. — Flutter's keyLabel returns the printable representation, not a readable name. Added _displayLabelForKey() with a table that maps whitespace and common special keys to canonical English labels matching the entries in crates/chanora_audio/src/ptt_backends/windows_keymap.rs (so the bridge resolves to the right VK_* on Windows). Pure modifier keys (shift / ctrl / alt / meta / caps / num / scroll lock) return null so they don't accidentally bind on their own. Issue 2: physical key press -> 'PTT=on' in the Voice Bar lagged by up to ~500 ms because audioStats was polled at 500 ms intervals. The Rust-side transition is microsecond-fast; the visible delay is purely the Flutter poll interval. Cut to 80 ms (~12 Hz), well below the perceptual lag threshold. Adds ~12 small FFI calls per second, trivially cheap. A push-based BridgeEvent::TransmitActiveChanged would let us drop the poll entirely; noted as a follow-up. flutter analyze: clean (6 pre-existing Radio.groupValue infos). |
||
|
|
87cff52c0a |
test(ptt): acceptance tests for press-on / release-off shape
Two new tests covering the user-acceptance criterion 'press → ptt
on, release → ptt off' through the full pipeline (backend press_gate
→ edge watcher → release tail → selector → real gate, identical to
what audioStats.pttActive reads in production):
* press_on_release_off_zero_tail — release_tail_ms = 0, transitions
are synchronous modulo one tokio tick.
* press_on_release_off_default_tail — release_tail_ms = 200,
transmit stays on briefly past key-up then transitions off.
cargo test --workspace --lib: 76 passed / 0 failed / 1 ignored.
|
||
|
|
82b99ebcff |
feat(ui,voice): add speaker mute button to VoiceBar
Speaker (output) mute existed in the legacy _AudioControls widget and the rust.setOutputMuted bridge call but was lost when SDD-097 replaced _AudioControls with VoiceBar. Mic mute carried over; speaker mute did not. Wire it back: VoiceBar gains an outputMuted prop + onToggleOutputMute callback and renders a headset/headset_off icon next to the existing mic mute. main.dart wires the existing _toggleOutputMute handler (previously dead-code with // ignore: unused_element). The bridge call setOutputMuted already does both effects together: local engine silencer + server-broadcast ClientOutputMuted flag. l10n: rename voiceHardMuteLabel to 'Mute microphone'/'麦克风静音' to distinguish from the new voiceOutputMuteLabel 'Mute speakers'/ '扬声器静音'. flutter analyze: clean (6 pre-existing Radio.groupValue infos). |
||
|
|
33d80894d0 |
feat(ptt,audio): wire PTT key edges through ReleaseTailTimer (SDD-096)
Previously the PttController handed its real AudioTransmitGate to the platform backend and the backend wrote transmit_active directly on every key edge — bypassing the 200 ms release tail and the TransmitMode selector entirely. The tail timer was constructed and exposed on ChanoraSession but never received any input, so SDD-096 and SRS-206 were spec-only. Wire it: PttController now owns a synthetic 'press-edge gate' which it hands to the backend in place of the real one. An internal edge-watcher task subscribes to that press-gate, translating true/false transitions into ReleaseTailTimer.key_down/key_up calls. The release-tail timer feeds the selector's ptt_held input; the selector recomputes transmit_active honouring mode, in_channel, and hard_mute, and writes the real gate. Single owner of transmit_active is preserved (SAD-083 invariant). PttController::new now takes Arc<ReleaseTailTimer> instead of AudioTransmitGate; ChanoraSession threads its session-scoped timer through both the start_audio path and the supervisor reconnect path. The legacy bridge set_ptt call (still used by the in-focus Listener fallback and the e2e test) is rerouted through the timer so the same tail and mute semantics apply uniformly. ReleaseTailTimer gains force_release() — cancels any pending task AND clears the selector's ptt_held. PttController::stop uses it so shutdown can't leave transmit_active stuck at true. Tests - press_edge_drives_selector_through_release_tail: backend press edge → real gate follows, key_up → tail keeps gate true for tail window then clears. - stop_clears_press_and_cancels_tail: stop() drops transmit even with a tail in flight. - e2e test now sets release_tail_ms=0 + waits one tick so the pttActive=false assertion isn't racing the default 200 ms tail. cargo test --workspace --lib: 74 passed / 0 failed / 1 ignored (+2 new tests vs. the previous 72). flutter analyze: clean (6 pre-existing Radio.groupValue infos). |
||
|
|
64878e3a7d |
fix(ui,voice): consolidate to single Voice settings entry point
The capability badge had its own 'Configure' TextButton that opened the bind-key flow, while the Voice Bar's settings gear also reached bind-key through the settings dialog. Two paths, same destination — confusing and pointless duplication that the user flagged. Resolution: the gear is the only configuration entry point. The capability badge becomes information-only — it still shows the detected PTT level + backend and (for L0Focused) the info-icon explanation sheet, but no Configure button. The badge no longer takes or props. The Voice Bar drops the callback added in |
||
|
|
8cd919cffc |
fix(audio,storage,ui): allow PTT binding before audio is running
Save-binding before joining a voice channel used to return BridgeError.invalidCommand(audio not started) because the PttController only exists after start_audio runs and set_ptt_binding required a live controller. Users naturally want to bind their PTT key once on first launch, not every time they join a channel — fix: * chanora_storage::IdentityFileStore::set_ptt_binding / get_ptt_binding persist the privacy-safe binding triple (input_class, platform_key, key_label) into audio_meta.json next to transmit_mode and release_tail_ms. * ChanoraSession holds pending_binding: Arc<Mutex<Option<PttBinding>>>. set_ptt_binding now (1) persists to storage best-effort, (2) stashes into pending_binding, (3) forwards live to the controller only if one exists. No more AudioNotStarted. * init_storage loads the persisted binding into pending_binding so it survives app restarts. * start_audio applies pending_binding immediately after constructing the PttController so the first key-press after join already works. * supervisor_loop carries pending_binding and re-applies it after any reconnect-driven audio engine restart, so reconnects don't silently drop the hotkey. * New bridge call get_ptt_binding() -> (input_class, key_label) plus a matching Flutter _hydratePttBinding() in initState lets the Voice Bar show the user's saved hotkey label on launch (e.g. 'PTT: Space') before any voice channel is joined. cargo test --workspace --lib: 72 passed / 0 failed / 1 ignored. flutter analyze: clean (6 pre-existing Radio.groupValue infos). FRB bindings regenerated. |
||
|
|
6a41a0b4db |
fix(ui,voice): five Voice Bar / settings UX bugs
1. Hard-mute now informs the server (setInputMuted) in addition to clamping the local TransmitGate. Without the server-side flag, other clients keep seeing us un-muted; without the local clamp a beat of in-flight audio leaks through. Drive both together so the mic icon and the actual silence land at the same time. 2. Split the badge's Configure affordance from the Voice Bar's 'Voice settings' gear. The gear opens the mode + release-tail dialog (onConfigure); the badge's configure opens the bind-key capture flow directly (new onBindKey). Previously both routed to the settings dialog, so 'Voice settings' and the badge's 'Configure' were the same screen — useless duplication. 3. Bind-key label is now PTT-only. The mode-badge row no longer prints 'PTT: Space' when Continuous / Voice Activity is selected. A new PTT-only secondary line carries the bound key plus the release-tail value together, hidden entirely for non-PTT modes. 4. Release-tail row is now PTT-only in BOTH the Voice Bar and the Voice settings dialog. The dialog previously kept the slider visible across all modes; switching to Continuous left the user staring at a control that did nothing. 5. PTT capability badge is now PTT-only. In Continuous and Voice Activity modes there is no key binding to surface a capability for, so the 'L0Focused (focused)' line + its info sheet and the Configure button disappear from the Voice Bar when the user isn't in PTT mode. All five fixes are pure UI; no Rust changes needed. flutter analyze remains clean (6 pre-existing Radio.groupValue deprecation infos). |
||
|
|
7f4874f4c0 | chore(bridge): regenerate FRB bindings for log_file_path_str | ||
|
|
6a077ac7a1 |
feat(bridge): tee tracing output to platform log file + log_file_path_str API
Bridge already had a tracing fmt layer writing to stderr but a Flutter
desktop app launched from Explorer / RDP has no terminal attached so
those records vanish. Add a non-ANSI file appender (best-effort,
4 MiB rotation) at the platform-conventional log path so developers
and beta testers can hand-inspect output:
* Linux: $XDG_STATE_HOME/app.chanora/chanora_flutter/chanora.log
(fallback ~/.local/state/...)
* macOS: ~/Library/Logs/app.chanora.chanora_flutter/chanora.log
* Windows: %LOCALAPPDATA%\app.chanora\chanora_flutter\logs\chanora.log
Expose log_file_path_str() over FRB so the UI can show the path in a
'Save diagnostics' affordance later. Mobile (Android/iOS) returns an
empty string — those platforms still rely on logcat / Console.app.
No DEC-016 conflict: this is local-only, append-only, never
auto-uploaded. The in-memory log sink and export_diagnostics() path
are unchanged. The redacting layer still wraps the in-memory sink;
the new file appender consumes the same tracing events post-filter.
Trigger for this change: a ko-KR Windows 11 tester saw
'BridgeError.invalidCommand(audio not started)' with no way to find
the upstream warn record that documents which cpal call failed. Log
file is now discoverable without a terminal launch.
|
||
|
|
ba444d94bd |
feat(audio,bridge,flutter): v1 audio + PTT lifecycle implementation (SDD-094..097)
Implement the SDD-094 / SDD-095 / SDD-096 / SDD-097 detailed designs
committed in
|
||
|
|
dfa84ee7bb |
docs(spec): baseline 0.9.5 — v1 audio + PTT lifecycle redesign
Add SysRS-303/304, SysDes-149/150/151, SRS-204/205/206/207, SAD-081/082/083, SDD-094/095/096/097, DEC-029/030. Captures the v1 lifecycle redesign: - Drop manual Start-audio button; audio engine is bound to voice-channel join/leave (ensure_running on first join, shutdown_if_idle on last leave). Output stream opens regardless of mic-permission state so listen-only is a first-class flow. - TransmitMode enum (Ptt / Continuous / VoiceActivity-reserved). Default Ptt on fresh install. Persisted per identity. - PTT release tail: 200 ms default (0-500 ms configurable) before transmit gate closes, avoiding clipped trailing syllables. - Hard-mute toggle overrides transmit gate regardless of mode/PTT. - Bridge surface: drop start_audio/stop_audio; add voice_join(channel_id) / voice_leave() and BridgeEvent::VoiceState. DEC-029 rejects Flutter global-hotkey packages (hotkey_manager, super_hot_key) for PTT: they wrap RegisterHotKey/RegisterEventHotKey which consume the key and don't fire key-up, wrong primitive for PTT. Native Rust DesktopPttBackend (SDD-083/084/085) stays authoritative. DEC-030 defers Voice Activity Detection to P1. RMS / WebRTC VAD / Silero VAD trade-off review (binary-size, dependency-surface, CPU profile) postponed; TransmitMode::VoiceActivity reserved on the enum surface so a P1 increment is non-breaking. Validator clean: 304/151/207/83/97 IDs, strict layered sourcing preserved, no new warnings beyond the pre-existing 35 old-package-name filenames. |
||
|
|
f9d20d8585 |
fix(audio,windows): adapt Raw Input + hook backends to windows-rs 0.54 API shape
Initial Task C commit (
|
||
|
|
77c2a1def4 |
feat(audio,windows): real Raw Input + low-level hook global PTT (SDD-083 / SDD-084)
The v1.0.0-rc.7 Windows backends were thread::sleep stubs that
reported optimistic L2GlobalHoldToTalk / L3GlobalWithMouseButtons
descriptors without actually registering for any global key events.
Surfaced on Windows verification as:
* 'even press to talk key was set, still only the hold to talk
button is work for talk'
* 'and displayed as L2GlobalHoldToTalk(raw-input)'
* 'cannot continuous transmission'
This commit implements the real backends:
WindowsRawInputBackend (preferred Windows rung, SDD-083):
* Hidden message-only window via
CreateWindowExW(..., HWND_MESSAGE, ...).
* RegisterRawInputDevices with RIDEV_INPUTSINK on
Usage Page 0x01 / Usage 0x06 (keyboard) and 0x02 (mouse) so
events fire globally — including when Chanora is unfocused.
* WndProc handling WM_INPUT: GetRawInputData ->
keyboard.VKey vs bound vk, or mouse.usButtonFlags vs bound
side-button index. Down -> gate.set(true); up -> gate.set(false).
* Dedicated chanora-rawinput thread runs GetMessageW /
TranslateMessage / DispatchMessageW until stop() posts
WM_QUIT via PostThreadMessageW.
WindowsHookBackend (fallback rung, SDD-084):
* SetWindowsHookExW(WH_KEYBOARD_LL) + WH_MOUSE_LL on a
dedicated chanora-llhook thread.
* Hook procs translate KBDLLHOOKSTRUCT.vkCode and
MSLLHOOKSTRUCT.mouseData against the same shared
AtomicBinding.
* UnhookWindowsHookEx on teardown.
Both backends:
* Honest descriptor() reporting: backends start reporting
L0Focused; level upgrades to L2 / L3 only after a real
arming success (RegisterRawInputDevices or SetWindowsHookEx
returning Ok). This fixes the 'L2 reported but doesn't fire'
complaint by making the badge tell the truth — if Raw Input
registration fails at runtime the user sees the L0Focused
info-icon explanation sheet instead of being told L2 works.
* AtomicBinding (class / vk / mouse_btn) for lock-free hot
path. Translation lives in
crates/chanora_audio/src/ptt_backends/windows_keymap.rs
which maps Flutter LogicalKeyboardKey.keyLabel strings
(e.g. 'Space', 'F10', 'A') to Win32 VK_* codes; mouse
side-button bitmask strings ('mouse-side-button:8' /
':16') to RawInput button indices (4 / 5).
* Per-thread context (thread_local RefCell) carries the
gate + binding to the WndProc / hook proc without needing
raw-pointer user-data plumbing.
Diagnostic logging:
* AudioEngine::start now logs default_input_config and
default_output_config explicitly with the channels /
sample_rate / sample_format that cpal reports, so a
build_*_stream failure on locale-specific Windows hosts
(reported on ko-KR Windows 11 as 'Start Audio Button not
work') becomes diagnosable from the stderr log alone.
* build_output_stream surfaces the requested config in the
tracing::error! record on failure.
Privacy (DEC-027 / SDD-090): the windows.rs and
windows_keymap.rs hot paths NEVER log raw VKs, scan codes,
keysyms, key labels, or button identifiers. Only the
platform-neutral input class ('keyboard' /
'mouse-side-button') and the backend id appear in the tracing
stream. The SDD-090 PttSanitizer Layer is the defence-in-depth
net but this code does not rely on it.
Tests: 4 new windows-only unit tests in windows_keymap (ASCII
letters / digits / Space + Fn / unknown / mouse button index).
They compile only under cfg(target_os = "windows") so the
Linux workspace test count is unchanged at 59/0/3.
Cargo deps: adds windows = '0.54' (target_os = windows) with
the feature set needed for RawInput + hooks. 0.54 matches
the version already transitive through the workspace.
Verified on Linux: cargo check --workspace clean, cargo test
--workspace 59/0/3 (windows-gated tests skip on Linux). The
real exercise of this commit will happen on the Korean Windows
11 host (100.84.219.45) at the next build.
|
||
|
|
8e04a1e2a6 |
fix(storage): file is durable DEK source; keyring is accelerator only
Surfaced on the v1.0.0-rc.7 Windows verification round as 'Bridge
Error connection failed: storage crypto decrypt aead error'.
Root cause: IdentityFileStore::ensure_dek treated the platform
keyring as the authoritative store and deleted identity.dek after
successfully promoting it. Subsequent launches whose process
context could not reach the keyring (Windows SSH session hits
ERROR_NO_SUCH_LOGON_SESSION; macOS LaunchAgent contexts hit
errSecMissingEntitlement) saw dek_path.exists() = false and
generated a fresh DEK, even though the keyring still held the
DEK that originally encrypted identity.tskey. Next ChaCha20-
Poly1305 AEAD decrypt of the identity blob then failed because
the in-process DEK was 32 fresh random bytes, not the bytes that
encrypted the stored ciphertext. The bookmark store (which
shares the DEK via crypto()) also broke for the same reason.
Manual reproduction on the rc.7 build at 100.84.219.45:
* Launch via SSH (keyring unreachable) -> file DEK_v1 created,
identity.tskey eventually encrypted under DEK_v1.
* Launch via RDP (keyring reachable) -> file DEK_v1 promoted
to keyring, identity.dek deleted.
* Launch via SSH again (keyring unreachable, file gone) -> a
fresh DEK_v2 is written to file. identity.tskey still
encrypted under DEK_v1.
* Next decrypt: DEK_v2 vs identity.tskey ciphertext -> AEAD
tag mismatch -> StorageError::Crypto('decrypt: \u2026') ->
bubble up as 'storage crypto decrypt aead error'.
Fix invariants:
* identity.dek (file) is the durable source of truth and is
never deleted by ensure_dek.
* keyring is opportunistic: we copy the DEK into it for the
UX-level convenience of platform-managed secret storage,
but its presence/absence does not affect correctness.
* ensure_dek on first install writes the DEK to BOTH places.
* ensure_dek on subsequent launches: keep using the file DEK;
re-copy into the keyring if not present (idempotent).
* load_dek prefers the file; only consults the keyring as a
legacy-migration fallback for installs that lost their file
mirror before this commit landed.
No SDD / SAD / SRS contract changes - the file fallback at
identity.dek and the in-keyring entry at app.chanora.identity::
identity-dek::<canonical-dir> were both already documented
behaviours; this commit corrects which one is authoritative.
The file lives in app-private storage where the platform
sandbox is the access-control authority (this was already
called out in the existing open_private comment on non-Unix
targets), so retaining the file mirror does not weaken the
security posture in any meaningful way relative to the prior
keyring-only durable-state design.
Verified on Linux: cargo test --workspace 59/0/3 (no regressions
from
|
||
|
|
5c413ba199 |
fix(protocol): sort channels by TS3 linked-list order, not by numeric value
The TeamSpeak 3 protocol's per-channel `order` field is NOT a
numeric rank — it stores the ChannelId of the channel that should
appear immediately before this one within the same parent. The
previous chanora_protocol::adapter::build_snapshot sorted by
`order.0` as if it were a sequence number, producing
stable-but-arbitrary output that did not match TS3 client display
order. Surfaced on the Windows verification round as 'channel
sort in not correct'.
Replace the numeric sort with a linked-list walk per parent
followed by a root-first depth-first emission so the bridge
consumer receives a pre-ordered tree:
fn sort_channels_tree(&[&Channel]) -> Vec<&Channel>
fn sort_channels_tree_by<T>(&[&T], extract) -> Vec<&T>
fn emit_subtree<T>(by_parent, root_id, out, extract)
Defensive behaviour:
* Per-parent cycle guard so a malformed snapshot can't infinite-loop.
* Channels whose predecessor pointer is unreachable from
order=0 are appended at the end of their parent bucket sorted
by id (channel never silently disappears from the UI).
* Channels whose `parent` is not present anywhere in the tree
are appended at the very end sorted by id (orphan defence).
Unit tests cover the four shapes that broke real users:
* Single-parent linked list out of HashMap iteration order
* Disconnected predecessor (leftover-bucket fallback)
* Two-level tree (depth-first subtree emission)
* Two-channel cycle (no infinite loop, both channels emitted)
Also removes the now-redundant Dart-side numeric sort in
_SnapshotView.build(); Flutter trusts the pre-ordered server
list and would otherwise re-introduce the bug.
Verified on Linux: cargo test --workspace 59/0/3 (was 55 + 4 new
adapter tests), flutter analyze clean.
|
||
|
|
feceacfdad |
feat(flutter): surface bound PTT key label in capability badge (SDD-091 follow-up)
After a user saved a PTT binding through _PttBindingCaptureDialog,
the capability badge showed the resolved level + backend (e.g.
'PTT: L2WindowsRawInput (windows-raw-input)') but never told the
user which key they had actually bound. Reported on the Windows
verification round as 'do you think we should tell user what key
they have set and then they will know what to press'.
This commit caches the captured platform-neutral key label in
_BetaHomeState whenever _onConfigurePtt succeeds, and threads it
through _AudioControls to a new boundKeyLabel prop on
PttCapabilityBadge. When the prop is non-empty the badge renders
a second line below the existing row:
PTT: L2WindowsRawInput (windows-raw-input) [ⓘ] [Configure]
Key: Space
The label uses bodySmall + monospace + onSurfaceVariant to stay
visually subordinate to the capability descriptor. Two new l10n
entries (en + zh) cover the 'Key: {key}' string.
Privacy: the displayed label is the same platform-neutral
LogicalKeyboardKey.keyLabel string the dialog already shows
during capture and that already crosses the bridge as
PttBinding.platform_key. No raw OS key code is introduced
(DEC-027 / SDD-077 compliance preserved).
State scope: display-only cache that resets on app restart. The
bridge-side PttController (SDD-088) holds the authoritative
binding; this UI cache is purely for display continuity within
a single process.
Verified on Linux: flutter analyze clean, cargo test --workspace
55/0/3.
|
||
|
|
9831624079 |
feat(ptt): close P0 unit-boundary gaps (SDD-088 / SDD-090 / SDD-091)
The P0 audit on v0.9.4-docs found three SDD items whose specified
software units were inlined into other types rather than packaged as
named units at the SDD-defined boundary:
* SDD-088 PttController — backend ownership + binding mutex +
capability watch lived split between AudioEngine and
ChanoraSession. Extracted into chanora_core::ptt::PttController.
AudioEngine now owns only the cpal streams and the missed-key-up
watchdog (SDD-092); the platform input backend, the active
PttBinding, and the capability watch::Sender live in the
controller. ChanoraSession::start_audio constructs the controller
against the engine's gate; disconnect/reconnect/restart paths
tear it down through stop().await before the engine.
* SDD-090 PttSanitizer — banned-field check was inlined as
PttBanCheckVisitor inside RedactingLogLayer::on_event. Extracted
into a generic PttSanitizer<L> tracing_subscriber::Layer that
decorates an inner Layer (canonical pairing:
RedactingLogLayer::with_sanitizer). The inner layer keeps its
own structural ban check as defence-in-depth for bare-install
callers.
* SDD-091 PttCapabilityBadge — Voice Bar badge was anonymous
Padding/Tooltip/Row inside _AudioControlsState.build. Extracted
into a public PttCapabilityBadge widget and added the
SDD-091-specified per-platform explanation sheet that opens on
the info-icon tap when the resolved capability is L0Focused.
New l10n strings (en + zh) cover the sheet copy.
Tests:
* 2 new unit tests for PttController (arm + descriptor watch)
* 1 new unit test for PttSanitizer (end-to-end through a real
tracing subscriber proving banned drop + safe forward)
cargo test --workspace: 55 passed / 0 failed / 3 ignored
cargo deny check: advisories ok, bans ok, licenses ok, sources ok
flutter analyze: no issues
tools/validate_docs.py: zero undefined refs, zero direct-layer
violations (pre-existing 35 old-package-name warning unchanged)
No SDD/SAD/SRS doc changes — the contracts already named these
units; this commit aligns code unit boundaries with those contracts.
|
||
|
|
63f2901a6a |
docs(traceability): close SRS-200 SAD/SDD coverage gap (P0 audit follow-up)
A P0 traceability audit per the project's compliance workflow
found exactly one gap across the 162 P0 SRS items:
* SRS-200 (mouse side buttons as bindable inputs for desktop
Global PTT) had no dedicated SAD item. The earlier baseline
matrix folded SRS-200 under the narrative
`SRS-195..203 -> SAD-071..079` umbrella, which technically
covered the requirement at the table level but did not give
SRS-200 a one-to-one SAD source the validator's strict
discipline expects.
Per the project's gate-check workflow (Case C —
BLOCKED_MISSING_SAD) this commit closes the documentation chain
*before* claiming compliance for the already-merged mouse-side-
button code path:
* `docs/architecture/sad.md` v0.9.4 — new `SAD-080` sources
SRS-200, allocates `Audio (Windows / macOS / Linux), Bridge,
Flutter UI`, and records the cross-platform mouse-side-button
surface as a software-architecture item. The §27 coverage
matrix gains a dedicated SRS-200 -> SAD-080 row.
* `docs/architecture/sdd.md` v0.9.4 — new `SDD-093` sources
SAD-080. Specifies the `PttInputClass` enum surface
(`None` / `Keyboard` / `MouseSideButton`), the rebind
contract on the Windows + macOS backends, the Linux portal's
pass-through behaviour, and the Flutter
`PointerEvent.buttons` bitmask capture (back = `0x08`,
forward = `0x10`). The §11 coverage matrix gains the
SAD-080 -> SDD-093 row.
* `docs/governance/traceability-matrix.md` v0.9.4 — the
`(DEC-026 mouse buttons)` row moves from
`SAD-072..074 / SDD-082..086` to the dedicated
`SAD-080 / SDD-093`.
* `docs/governance/baseline-candidate-validation-report.md`
v0.9.4 — ID totals advance to 302 / 148 / 203 / 80 / 93;
direct-layer-rule and undefined-reference counts remain
zero.
* `docs/governance/repo-format-validation-report.md` v0.9.4 —
same ID totals update.
Audit summary
-------------
* 162 P0 SRS items audited.
* 1 SAD-coverage gap (SRS-200) — closed by this commit.
* 0 SDD-coverage gaps (every PTT SAD has explicit SDD coverage;
the inherited baseline `SAD-032/033` are covered through the
documented range row, not individually).
* All Priority: P0 software requirements now have a strict
SRS -> SAD -> SDD chain on file.
Validator output:
```
[OK] SysRS: 302 defined, 0 undefined references
[OK] SysDes: 148 defined, 0 undefined references
[OK] SRS: 203 defined, 0 undefined references
[OK] SAD: 80 defined, 0 undefined references
[OK] SDD: 93 defined, 0 undefined references
[OK] SRS direct SysRS references: 0
[OK] SAD direct SysRS references: 0
[OK] SAD direct SysDes references: 0
[OK] SDD direct SysRS references: 0
[OK] SDD direct SysDes references: 0
[OK] SDD direct SRS references: 0
```
Implementation status
---------------------
The mouse-side-button code was implemented in v1.0.0-rc.4 and
v1.0.0-rc.5 under the (then-implicit) PTT umbrella; the code
already matches the new SDD-093's contract verbatim. This
commit ships **documentation only** — it adds the SAD/SDD/matrix
rows that retroactively justify the existing implementation
under the strict traceability discipline. No code edits, no test
edits.
* `cargo test --workspace`: 67/67 green (unchanged).
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate`: zero new warnings (no Cargo.lock
delta).
* `flutter analyze`: clean.
* `tools/validate_docs.py`: all SRS/SAD/SDD coverage and
direct-layer-rule checks pass.
Outstanding open items remain live verification per
`docs/release/release-readiness-go-nogo-record.md`
(RR-PTT-001..006/008) and the DEC-012 legal review; both are
non-engineering work.
|
||
|
|
03f5d6bca3 |
fix(p0): close three P0 coverage gaps after rc.5 audit
Audited every `Priority: P0` row in `docs/requirements/{sysrs,srs}.md`
against the live code. Three items needed work; this commit closes
all three.
Gap A — SysRS-262 + SysRS-282 (screen-reader semantics + accessible
labels for the PTT control)
-------------------------------------------------------------------
The Flutter PTT control is a custom `Listener` over a `Container`
— not a built-in `Button`, so the platform accessibility tree had
no idea it was an interactive control. Screen readers
(VoiceOver, TalkBack, NVDA, Orca) would have read the visible text
without announcing the control role or its toggled state.
Wrap the Listener in a `Semantics(button: true, toggled: _pressed,
label: …, hint: …, excludeSemantics: true)` so the platform
accessibility tree carries the right role, the current state
("Hold to talk" / "Transmitting"), and a usage hint. The
`excludeSemantics: true` argument suppresses the duplicate child
nodes the Container + Row + Icon + Text would otherwise generate
on top of our explicit label.
SysRS-263 (no colour-only state) is preserved: the visible label
and the mic icon already differentiate the two states without
relying on the colour transition.
New ARB key `pttHoldToTalkSemanticsHint` in `app_en.arb` and
`app_zh.arb`.
Gap B — SRS-198 (macOS async permission re-check)
-------------------------------------------------
The macOS backend queried `query_permission()` once at
construction and never re-checked. That violates SRS-198's
"upgrade to the appropriate Global level only after the user
grants the required permission" — once Chanora is running, a
runtime grant must lift the descriptor from `L0Focused` to a
Global level without an app restart.
Substantive rewrite of `crates/chanora_audio/src/ptt_backends/macos.rs`:
* `permission: PermissionState` becomes `permission: Arc<AtomicU8>`,
enabling cross-thread updates without a Mutex.
`PermissionState::{to_u8, from_u8}` carry the encoding.
* The backend owns a `tokio::sync::watch::Sender<PttBackendDescriptor>`
and overrides `DesktopPttBackend::descriptor_watch()` to hand
out subscribers; `chanora_core::ChanoraSession::start_audio`
already forwards transitions to `SessionEvent::PttCapability`.
* `start()` spawns a `chanora-perm-watch` OS thread that polls
`query_permission()` every 1.5 s and republishes the
descriptor on every transition. Polling rather than KVO /
notifications because Input-Monitoring has no public
change-notification API on macOS; 1.5 s is sufficient for a
user grant + return-to-Chanora cycle.
* `rebind()` also republishes the descriptor so a
`keyboard → mouse-side-button` change updates the badge.
* Six new unit tests on the platform-independent
`build_descriptor` and the atomic encoding contract. They
only compile under `target_os = "macos"` (consistent with
the rest of the module), so the Linux dev-host workspace
test count is unchanged.
`query_permission()` itself still returns `Undetermined` until
the IOKit live link lands in the macOS platform-verification
commit; the re-query loop will engage the upgrade path
automatically the moment that function returns real values.
Gap C — SRS-200 (Linux mouse-side-button portal-dependence)
-----------------------------------------------------------
`desktop-ptt-architecture.md` §5.3 already described the
heuristic classifier. Added one explicit sentence stating that
Linux mouse-side-button support is *portal-dependent*: Chanora
never claims a fixed Mouse4/Mouse5 binding on Linux; the portal
decides what inputs it accepts in the current session, and the
classifier degrades to `keyboard` whenever the portal's
description does not contain "mouse". This matches the SRS-200
text verbatim and removes the ambiguity over what "Linux
support follows the portal" means in practice.
Verification
------------
* `cargo test --workspace` (with `CHANORA_DISABLE_KEYRING=1`):
all 67 Linux-side tests green (unchanged). The new macOS
unit tests count under `target_os = "macos"` only — they
will report once the macOS reference host runs `cargo test`.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `flutter analyze`: clean (no new accessibility warnings).
* Linux release bundle builds clean.
P0 audit summary
----------------
After this commit every Priority: P0 row in `sysrs.md` and
`srs.md` has a concrete implementation. The remaining open items
are all live verification, not code:
* Per-platform live PTT traces on Windows / macOS reference
hosts (RR-PTT-001..003, RR-PTT-008) — hosts unavailable
locally; queued for platform owners.
* Linux GNOME-Wayland live trace (RR-PTT-004) — implemented
in rc.5; awaiting live host trace.
* Linux non-tested compositor fallback trace (RR-PTT-005) —
Open.
* Diagnostic-export key-leak inspection (RR-PTT-006) — Open
but trivially testable on any host with PTT bound.
* DEC-012 legal review — engineering hand-off complete since
rc.2.
|
||
|
|
82d012a46b |
feat(ptt): live Linux GNOME-Wayland portal session flow (DEC-025)
Promotes the Linux backend from probe-only to a live
`org.freedesktop.portal.GlobalShortcuts` session, closing the
gen2 v0.9.3 baseline's last Linux-side code item. Both gaps I
flagged on the review pass are addressed:
* Stop now closes the portal session through the dedicated
`org.freedesktop.portal.Session` interface (not the
request-cancel `Request` interface — that would only abort a
pending Request, not release the bound shortcuts).
* Ten new unit tests cover `classify_shortcuts_value`,
`publish_bound`, `publish_l0`, and the `SHORTCUT_ID` stability
contract using synthesised `OwnedValue` payloads. Live D-Bus
coverage stays in the `linux_portal_smoke` ignored
integration test (RR-PTT-004).
Live session lifecycle (gen2 Q5b — lazy, single backend instance):
1. `start(gate, binding)` spawns one `tokio::spawn` worker that
owns an async `zbus::Connection` (sharing the bridge's
tokio runtime per Q4a).
2. `CreateSession` with fresh random `handle_token` /
`session_handle_token` tokens. The worker awaits the portal
`Response` signal via a `RequestProxy` subscription and
extracts `session_handle` from the results dict.
3. `BindShortcuts(session_handle, [("chanora-ptt", { description
= "Chanora push-to-talk" })], "", {})`. The portal opens its
own system-managed dialog asking the user to choose a key
— Chanora itself never reads raw key events. The audio
engine continues at `L0Focused` while the dialog is open;
the descriptor watch publishes the transition once the
portal returns.
4. On `response_code == 0`: classify the `trigger_description`
substring (heuristic: contains "mouse" -> MouseSideButton,
else Keyboard), publish `L2GlobalHoldToTalk` (or `L3` for
mouse) through the watch sender. The raw trigger_description
string is never logged (DEC-027 / SRS-202).
5. On `response_code == 1` (cancelled) or `>= 2` (failure):
publish `L0Focused` through the watch sender. The user can
retry via the UI "Configure" button (gen2 Q6a).
6. The worker enters a `tokio::select!` loop multiplexing the
`cmd_rx` channel (Rebind / Stop) and the `Activated` /
`Deactivated` signals. Matching signals scoped to this
session handle and `chanora-ptt` shortcut id drive
`gate.set(true/false)`.
7. `Rebind` re-runs `BindShortcuts` on the same session.
8. `Stop` calls `org.freedesktop.portal.Session.Close()` on
the session-handle object path, clears the gate, exits.
UX (gen2 Q3a): when `_pttBackendId == 'gnome-wayland-portal'`,
the Flutter "Configure" button skips the in-app
`_PttBindingCaptureDialog` and shows a SnackBar telling the user
their desktop environment will open its own shortcut dialog.
The button delegates to `setPttBinding(keyboard, "portal")`
which nudges the backend; the portal handles the rest. New ARB
key `pttConfigurePortalRedirect` in en + zh-Hans.
Trait surface (cross-cutting):
* `DesktopPttBackend::descriptor_watch()` is a new trait method
with a default impl returning a never-firing receiver.
Backends with async capability transitions (only the Linux
portal backend today) override it to return the live watch
sender's receiver.
* `chanora_core::ChanoraSession::start_audio` subscribes to the
active backend's `descriptor_watch()` and spawns a forwarder
task that re-emits `SessionEvent::PttCapability` on every
transition. The initial value is emitted synchronously.
`Cargo.toml` (Linux-only):
* `futures-util` (std features, no executor) for stream
consumption on the portal signal subscriptions.
* `rand 0.8` for fresh per-process portal tokens.
* `zbus` continues at v5 with the `tokio` + `blocking-api`
features.
Tests
-----
* `chanora_audio` rises from 8 to 18 unit tests. New
coverage on the Linux module:
- `classify_returns_none_when_shortcut_id_missing`
- `classify_returns_keyboard_for_typical_trigger_description`
- `classify_returns_keyboard_when_trigger_description_missing`
- `classify_detects_mouse_substring`
- `classify_is_case_insensitive_on_mouse_substring`
- `publish_bound_keyboard_publishes_L2_with_keyboard_class`
- `publish_bound_mouse_publishes_L3`
- `publish_bound_none_publishes_L2_keyboard_default`
- `publish_l0_clears_descriptor`
- `shortcut_id_is_stable`
* Workspace total: 67 unit + integration tests, all green with
`CHANORA_DISABLE_KEYRING=1` (was 57 at v1.0.0-rc.4).
* New `crates/chanora_audio/tests/linux_portal_smoke.rs`
ignored integration test (RR-PTT-004 evidence path). Run on
a GNOME-on-Wayland host with
`cargo test -p chanora_audio --test linux_portal_smoke -- --ignored --nocapture`.
Documentation
-------------
* `docs/architecture/desktop-ptt-architecture.md` §5.3 rewritten
to describe the realised lifecycle; v0.9.4 change-history
entry added.
* `docs/governance/product-decision-register.md` v0.9.10
change-history entry recording the code-side promotion. No
decision rows mutate.
* `docs/release/release-readiness-go-nogo-record.md` RR-PTT-004
flipped from `Open` to `Implemented (live trace pending)`;
v0.9.5 change-history entry.
Verification
------------
* `cargo test --workspace`: 67/67 green.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate --offline`: zero new warnings.
* `tools/dump_flutter_licenses.sh`: 94 packages, 0 without
LICENSE.
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` +
`flutter build linux --release`: clean Linux x86_64 bundle.
* Live portal trace (RR-PTT-004) — **not run**. The dev shell
is a TTY without a Wayland session. The user will run the
ignored smoke test from inside a GNOME-on-Wayland session
when available.
No Windows / macOS / iOS live verification in this commit (hosts
unavailable). The Windows + macOS backend scaffolds remain in
place reporting their target capability honestly; live OS-call
wiring is queued for their respective platform owners'
reference hosts per `docs/governance/staged-release-plan.md`.
|
||
|
|
5199e3d005 |
feat(ptt): full desktop backend ladder + missed-key-up watchdog (gen2 v0.9.3 follow-up)
Lands SDD-081..088 + SDD-092 implementations on top of v1.0.0-rc.3.
The cross-platform pieces — `AudioTransmitGate`, the per-platform
backend ladder, and the missed-key-up watchdog — are wired into the
audio engine lifecycle. Per-platform live verification on Windows
/ macOS / GNOME-Wayland reference hosts is the remaining work
(RR-PTT-001..006/008 in `release-readiness-go-nogo-record.md`).
`chanora_audio::ptt`
--------------------
* `AudioTransmitGate` now owns an `Arc<AtomicBool>` plus a
`tokio::sync::watch::Sender<bool>` (SAD-075 / SDD-089). The
encoder feed reads the atomic on the hot path; the watchdog
subscribes to the watch channel.
* `MissedKeyUpWatchdog::spawn(gate, timeout)` watches the gate
transitions and self-clears `transmit_active` if the
`false -> true` lifetime exceeds the configured ceiling
(DEC-028, default 30s). Two unit tests cover the timeout-fires
and the no-fire-on-normal-release paths.
`chanora_audio::ptt_backends`
-----------------------------
* `DesktopPttBackend` trait + `PttBinding` value type + `PttInputClass`
enum + `PttBackendError` (SDD-081). `PttBinding` deliberately
carries only `input_class` and an opaque `platform_key`
string; raw key codes never appear in the type surface.
* `select()` factory (SAD-071): runtime ladder evaluation per
OS. Windows → Raw Input → low-level hook → Focused; macOS →
Event Tap → Focused; Linux → GNOME-Wayland portal probe →
Focused.
* `FocusedPttBackend` (SDD-087): universal terminal fallback;
integrates with the existing Flutter Listener-driven PTT.
* `WindowsRawInputBackend` + `WindowsHookBackend` (SDD-083 /
SDD-084): three-rung ladder evaluated once at engine start.
Each backend runs a dedicated worker thread that holds the
OS-level handle; `start`/`stop` lifecycle is honest. Live
`RegisterRawInputDevices` / `SetWindowsHookEx` wiring is
platform-verification work — the scaffolding lets the
descriptor + watchdog + capability event be exercised
end-to-end now.
* `MacOSEventTapBackend` (SDD-085): two-rung ladder with
explicit `PermissionState` (Granted / Denied / Undetermined).
`Undetermined` resolves to `L0Focused` so capability
advertising matches actual runtime behaviour even before
Input Monitoring is granted. Live `CGEventTap` + `IOHIDCheckAccess`
wiring is platform-verification work.
* `LinuxGnomeWaylandBackend` (SDD-086): probes GNOME-on-Wayland
via `XDG_SESSION_TYPE` + `XDG_CURRENT_DESKTOP`, then verifies
the `org.freedesktop.portal.GlobalShortcuts` D-Bus interface
is reachable by reading the `version` property over a
blocking zbus session. Reports `gnome-wayland-portal` /
`L2GlobalHoldToTalk`. Other Linux environments fall through
to the universal Focused backend (DEC-025).
`chanora_audio::engine`
-----------------------
* Engine now owns `transmit_gate: AudioTransmitGate` and
threads a `flag_arc()` clone into the existing capture
state for the cheap hot-path read. `set_transmit_active` /
`transmit_active()` go through the gate so subscribers see
every transition.
* `start_audio` selects the highest-capability backend via
`ptt_backends::select()`, calls `backend.start(gate, none())`,
and spawns the watchdog. Both are released in `stop()` and
on Drop.
* New `engine.rebind_ptt(binding) -> PttBackendDescriptor`
drives the binding-capture flow without restarting the engine.
* New `engine.ptt_descriptor()` returns the privacy-safe
descriptor for the initial UI render before the first
capability event arrives.
`chanora_core`
--------------
* Re-exports `PttBinding` + `PttInputClass`.
* New `ChanoraSession::set_ptt_binding(binding)` — calls
`audio.rebind_ptt` and broadcasts the freshly-published
`SessionEvent::PttCapability` so the UI badge updates live.
* New `ChanoraSession::ptt_descriptor()` for the initial render.
`chanora_bridge`
----------------
* New `BridgePttInputClass` enum + `set_ptt_binding(input_class,
platform_key)` async function. The `platform_key` string is
opaque to the bridge and never logged.
* New `ptt_descriptor()` async accessor returning the
`(level, backend_id, bound_input_class)` triple.
Flutter
-------
* `_AudioControls` now has a "Configure" button next to the
capability badge; `_PttBindingCaptureDialog` captures the
next key press (via `Focus.onKeyEvent`) or mouse side button
(via `Listener.onPointerDown` filtered to button bitmasks
`0x08` / `0x10`). The captured value is the platform-neutral
`LogicalKeyboardKey.keyLabel` or `mouse-side-button:{button}`.
* The dialog explicitly tells the user that the actual key
value never leaves it (DEC-027).
* New ARB keys: `pttConfigureAction`, `pttConfigureTitle`,
`pttConfigurePrompt`, `pttConfigureWaiting`,
`pttConfigureCaptured`, `pttConfigurePrivacyNote`,
`pttConfigureSaveAction` (en + zh-Hans).
Dependencies
------------
* `chanora_audio` adds (Linux only) `zbus = "5"` with the
`tokio` runtime selector + `blocking-api` feature for the
GlobalShortcuts portal probe.
* `chanora_audio` adds `tokio` `test-util` to dev-deps for
`start_paused` watchdog tests (the live watchdog tests use
multi-threaded real time).
Verification
------------
* `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`:
57 tests green (was 53). chanora_audio rises from 4 to 8.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate --offline`: regenerates
`docs/security/license-inventory.{md,html}`. The crate count
rises from 364 to 383 with the addition of the zbus tree.
* `tools/dump_flutter_licenses.sh`: 94 packages, zero without
LICENSE (unchanged).
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` + `flutter build
linux --release`: clean Linux x86_64 bundle.
Documentation
-------------
* `docs/release/release-readiness-go-nogo-record.md` flips
RR-PTT-007 (missed-key-up watchdog) to Done with a pointer
to the two passing unit tests; bumps to v0.9.4. Live
per-platform traces (RR-PTT-001..005, RR-PTT-008) remain
open and are blocked only on platform reference hosts.
Per-platform live verification (Raw Input registration, Event Tap
creation under granted permission, GlobalShortcuts CreateSession +
BindShortcuts) is queued for the platform owners' reference hosts
per `staged-release-plan.md`.
|
||
|
|
7b21916049 |
feat(ptt): code-side initial split — transmit_active / capability badge / sanitizer
Implements the gen2 v0.9.3 doc baseline's first slice of code work:
* SRS-201: split the audio engine's `ptt` AtomicBool into the
authoritative `transmit_active` flag. The legacy `set_ptt` /
`ptt` accessors are retained as `#[doc(hidden)]` thin wrappers
so the existing bridge command and the existing Flutter
hold-to-talk UI keep compiling.
* SAD-075 / SDD-089 acknowledged at the type level: only
`AudioEngine::set_transmit_active` (or its legacy alias)
mutates the flag; the encoder feed reads it once per outbound
frame and never writes.
* SDD-082: new `chanora_audio::ptt` module ships the
`PttCapabilityLevel` enum (`L0Focused`, `L1GlobalShortcut`,
`L2GlobalHoldToTalk`, `L3GlobalWithMouseButtons`,
`L4DeviceAware` reserved) with a stable `as_str` mapping and
an `is_global` classifier.
* SDD-087: `PttBackendDescriptor::focused()` constant value for
the universal Focused-PTT fallback. The struct shape carries
only privacy-safe fields (`level`, `backend_id`,
`bound_input_class`) — a key code cannot fit through this
surface by construction (DEC-027).
* SAD-077 / SDD-090: `RedactingLogLayer` now hosts the
`PttBanCheckVisitor` and the `PTT_BANNED_FIELDS` constant
(`key_code`, `scan_code`, `virtual_key`, `vk`, `keysym`,
`keysym_string`, `key_sequence`, `key_press_history`,
`key_timing`). Any record whose field set names a banned key
is dropped before reaching the in-memory log sink or the
user-initiated diagnostic export. The check is structural and
runs ahead of formatting / redaction.
* `SessionEvent::PttCapability` carries the diagnostics-safe
descriptor through the broadcast event stream;
`chanora_core::ChanoraSession::start_audio` publishes the
Focused-PTT descriptor when the audio engine starts (SRS-196
/ SDD-091).
* `BridgeEvent::PttCapability` mirrors the event across the
FFI boundary. flutter_rust_bridge codegen regenerated.
* Flutter `_AudioControls` renders a capability badge above the
PTT button: a globe icon for Global levels, a focus-frame
icon for `L0Focused`, plus a Tooltip exposing the bound input
class. New ARB key `pttCapabilityBadge(level, backend)` in
`app_en.arb` and `app_zh.arb`.
Per-platform global PTT backends (`WindowsRawInputBackend`,
`MacOSEventTapBackend`, `LinuxGnomeWaylandBackend`) and the
`MissedKeyUpWatchdog` task land in a separate follow-up commit;
this milestone ships only PTT-L0 universally so the application's
runtime capability reporting is honest from day one.
Tests
-----
* `chanora_audio` rises from 1 to 4 unit tests covering
`PttCapabilityLevel::as_str`, `is_global`, and the
`PttBackendDescriptor::focused()` shape contract.
* `chanora_diagnostics` rises from 9 to 11 unit tests covering
the new `PttBanCheckVisitor` over every banned field name and
the `PTT_BANNED_FIELDS` stability assertion.
* Workspace total: 53 unit + integration tests, all green with
`CHANORA_DISABLE_KEYRING=1` (was 49 at v1.0.0-rc.2).
* `flutter analyze`: clean.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate`: zero warnings (license inventory
regenerated).
* `tools/dump_flutter_licenses.sh`: 94 packages, zero without
LICENSE.
* Linux x86_64 release bundle builds clean.
No Android live verification in this commit per the user's note
that the test device was removed. Android arm64-v8a continues to
build via the same `cargo ndk` path; runtime reporting on Android
is `L0Focused` for the foreseeable future.
|
||
|
|
02ffadfa52 |
docs(ptt): land Baseline Candidate v0.9.3 — capability-based desktop PTT
Applies the gen2 desktop-PTT review summary
(`gen2/chanora-desktop-ptt-review-summary-v0.9.2.md`) to our doc set
with the owner rulings PTT-OPEN-001 through PTT-OPEN-006 resolved as
accepted decisions DEC-023 through DEC-028:
* DEC-023 Windows Global PTT P0 / MVP
* DEC-024 macOS Global PTT P0 / MVP with permission UX
* DEC-025 Linux officially-tested env: GNOME on Wayland only
* DEC-026 Mouse side buttons supported (Win + macOS; Linux portal)
* DEC-027 PTT diagnostics: capability + availability only, no
raw key codes ever
* DEC-028 Missed-key-up watchdog: P0
Requirements (SysRS / SRS) and architecture (SysDes / SAD / SDD)
gain the desktop-PTT ID set the gen2 summary describes:
SysRS-296..302 -> SysDes-142..148
-> SRS-195..203
-> SAD-071..079
-> SDD-081..092
ID totals advance from 295 / 141 / 194 / 70 / 80 to 302 / 148 / 203
/ 79 / 92. The strict layered sourcing rule (`SRS -> SysDes` only,
`SAD -> SRS` only, `SDD -> SAD` only) is preserved; the
`tools/validate_docs.py` validator reports zero undefined refs and
zero direct-layer-rule violations.
New document:
* `docs/architecture/desktop-ptt-architecture.md` — capability
ladder (L0Focused, L1GlobalShortcut, L2GlobalHoldToTalk,
L3GlobalWithMouseButtons, L4DeviceAware reserved), Windows /
macOS / Linux strategies, privacy rule, audio-gate rule,
missed-key-up watchdog, release-readiness evidence requirement,
traceability summary.
Doc addenda (Baseline Candidate 0.9.3):
* `privacy/privacy-policy.md` — no raw key history, capability-
dependent Global PTT, UI reflects actual runtime capability
* `security/threat-model.md` — THREAT-PTT-001..006
* `security/diagnostic-redaction-audit-report.md` —
REDACT-PTT-001..006 banned field list enforced by `PttSanitizer`
* `release/platform-release-policy.md` — per-platform evidence
fields, no over-claim on untested Linux compositors
* `release/release-readiness-go-nogo-record.md` — RR-PTT-001..008
release-readiness items
* `verification/swe4-unit-verification-plan.md` —
SWE4-UV-035..039
* `verification/swe5-software-integration-verification-plan.md` —
SWE5-IV-015
* `verification/swe6-software-verification-plan.md` — SWE6-SV-017
* `verification/sys4-system-integration-verification-plan.md` —
SYS4-SIV-016
* `governance/traceability-matrix.md` — full PTT trace rows +
verification map
* `governance/decision-impact-assessment.md` — DEC-023..028
impact matrix
* `governance/product-decision-register.md` v0.9.9 entry
recording DEC-023..028 in the decision table and the status
table at §7
* `governance/document-index.md` — adds
`desktop-ptt-architecture.md` to the controlled set
* `architecture/proof-of-concept-plan.md` —
PoC-PTT-001..005 platform items
* `references/external-references.md` — Windows Raw Input,
macOS event-tap, Linux GlobalShortcuts portal references
* Both validation reports
(`baseline-candidate-validation-report.md`,
`repo-format-validation-report.md`) bumped to v0.9.3 with the
new ID totals (302 / 148 / 203 / 79 / 92).
README §"Desktop Push-to-Talk" added between Architecture Overview
and Repository Layout: capability levels, per-platform strategy,
privacy posture, missed-key-up watchdog.
Tooling:
* `tools/validate_docs.py` copied from the gen2 zip into the
repo tree (was previously available only inside the zip).
Reports zero undefined refs, zero direct-layer-rule violations,
English-only CJK check passes. The 35 "old package-style
filename" hits are pre-existing and identical to the gen2
baseline (they live in `path-migration-map.md` and config-ID
headers of governance docs and are intentional per the path
migration policy).
* `.gitignore` adds `/gen2/` so the externally-provided review
package does not enter the repo.
No code changes in this commit; B (the implementation split into
`transmit_active` / `capture_active`, `PttCapabilityLevel`
reporting, `PttSanitizer` diagnostics rule, and the UI capability
badge) follows in a separate commit.
|
||
|
|
b932dc1405 |
feat(legal): land cargo-about + cargo-deny + Flutter license inventory
Closes engineering deliverables 1–3 from the open-work table in `docs/governance/legal-review-readiness.md` so the DEC-012 legal review can actually run. With this commit, the only remaining engineering item blocking sign-off is signed Windows / macOS / iOS build artefacts, deferrable per the DEC-002 staged release plan. Tooling ------- * `about.toml` + `about.hbs` + `about-md.hbs` configure cargo-about with the DEC-020 license posture and the five-target matrix (Linux, Android, Windows, macOS, iOS). One per-crate clarification for `allo-isolate` (`flutter_rust_bridge` transitive that ships Apache-2.0 via `license-file` rather than an SPDX `license` field). `cargo about generate` runs with zero warnings. * `deny.toml` mirrors the cargo-about allow-list and adds minimal bans / sources / advisories config. `cargo deny check` reports `advisories ok, bans ok, licenses ok, sources ok` for the workspace; multiple-versions of `windows_x86_64_msvc` produce advisory `warn` (no fail) because three windows-targets versions reach the graph via `jni`, `cpal`, and `keyring` respectively. * `tools/dump_flutter_licenses.sh` + `tools/dump_flutter_licenses.dart` walk `apps/chanora_flutter/pubspec.lock`, resolve each dependency to its local pub-cache directory, read the LICENSE file, and emit `docs/security/flutter-license-inventory.md`. SDK-sourced packages (`flutter`, `flutter_localizations`, `flutter_test`, `flutter_web_plugins`, `sky_engine`) resolve to the Flutter framework BSD-3-Clause LICENSE under `$FLUTTER_ROOT` (or `$HOME/sdks/flutter`). Artefacts --------- * `docs/security/license-inventory.md` — 364 transitive Rust crates with full license texts. Apache-2.0 (276), MIT (55), Unicode-3.0 (19), BSD-3-Clause (7), ISC (7). Zero copyleft. * `docs/security/license-inventory.html` — same data rendered as styled HTML for reviewer convenience. * `docs/security/flutter-license-inventory.md` — 94 Dart / Flutter packages with their LICENSE texts. Zero packages without a resolvable LICENSE in this RC. CI -- * New `supply-chain` job runs `cargo deny check --workspace --all-features` via `EmbarkStudios/cargo-deny-action@v2`. Fails the build on any GPL / LGPL / AGPL / commercial-source license surfacing transitively. * New `license-inventory` job installs `cargo-about --features cli` and regenerates `docs/security/license-inventory.md`; diffs against the committed copy and fails on drift. Forces contributors who touch the Cargo.lock to refresh the inventory. * New `flutter-license-inventory` job runs `tools/dump_flutter_licenses.sh` against the just-resolved pub cache; same diff-on-drift semantics. Governance ---------- * `docs/governance/legal-review-readiness.md` §5 cross-links the three new artefacts in a "Reviewer artefacts" subsection. * The open-work table at the bottom of the doc is rewritten as a status grid: items 1–3 now read **Done**; item 4 (signed iOS / macOS builds) remains the only open engineering blocker, with a pointer back to `staged-release-plan.md`. Verification ------------ * `CHANORA_DISABLE_KEYRING=1 cargo test --workspace`: all 49 unit + integration tests green (unchanged from v1.0.0-rc.1). * `cargo deny check`: advisories ok, bans ok, licenses ok, sources ok. * `cargo about generate --output-file …`: zero warnings. * `tools/dump_flutter_licenses.sh`: 94 packages, 0 without LICENSE. * `flutter analyze`: clean. No code changes touch the runtime; this is governance-tooling only. |
||
|
|
50768a8f48 |
feat(mvp): v1.0.0-rc.1 — keyring-backed DEK, encrypted bookmarks, MVP release-gate docs
Closes the v0.4 dual-file weakness in identity-at-rest and turns the release into an MVP public release candidate. The remaining work before `v1.0.0` is DEC-012 legal sign-off — see `docs/governance/legal-review-readiness.md` — and the staged platform promotions in `docs/governance/staged-release-plan.md`. No decision rows in `product-decision-register.md` change; the register's change-history advances to 0.9.8. `chanora_storage` ----------------- * New public `Crypto` trait + `IdentityFileStore::crypto()` give callers an encrypt / decrypt pair anchored on the per-install 32-byte DEK without exposing the key material. * `IdentityFileStore` keyring-first DEK retrieval (Linux Secret Service via D-Bus, macOS Keychain, Windows Credential Manager, iOS Keychain via the `keyring` crate). Pre-existing `identity.dek` files are opportunistically migrated into the keyring on first run; the on-disk DEK copy is removed once the keyring acknowledges. `CHANORA_DISABLE_KEYRING=1` forces the file-fallback path for tests and headless / CI hosts where a real keyring call would prompt the user or block on a missing D-Bus session. * `BookmarkRepository::with_crypto(dir, crypto)` encrypts the server password into a new `password_blob` BLOB column under the same per-install DEK. Schema v2 migration is idempotent — legacy v0.4 rows with a plain `password TEXT` are read transparently and lifted into `password_blob` on the next `update()`. `BookmarkRepository::new` (no crypto) is preserved for tests and as a documented fallback when the DEK is unreachable. * Storage tests rise from 8 to 10: encrypted bookmark password round-trip + legacy-plaintext-bookmark upgrade. `chanora_core` -------------- * `ChanoraSession::init_storage(dir)` wires the bookmark repository with crypto by default. On any crypto-derivation failure it falls back to the plain-password repository and logs the gap — better than hard-failing init. * `supervisor_loop` now tracks a 64-bit `snapshot_signature` over channels (id + parent + order + name) and clients (id + channel + name) instead of the old `(channel_count, client_count)` tuple. Any in-channel client move, channel rename, or reorder now fires `SessionEvent::SnapshotChanged`. The signature sorts by id before hashing so it's stable under input-vector reordering. * Two new unit tests cover the signature behaviour; new `tests/mvp_storage.rs` integration test drives `ChanoraSession::init_storage` end-to-end and verifies the bookmark `password_blob` does not contain the plaintext. * Re-export `ChannelId` + `ClientId` from `chanora_protocol` so downstream callers and tests can construct DTOs directly. Flutter ------- * New About dialog (info icon in the AppBar) surfaces DEC-018 (public name "Chanora"), DEC-019 (non-affiliation statement), and DEC-020 (Apache-2.0 OR MIT dual license). New ARB keys in `app_en.arb` and `app_zh.arb`: `aboutAction`, `aboutVersion`, `aboutNonAffiliation`, `aboutLicenseHeading`, `aboutLicenseBody`, `aboutThirdPartyHeading`, `aboutThirdPartyBody`. * `pubspec.yaml` version bumps to `1.0.0-rc.1+5`. Governance ---------- * `docs/governance/legal-review-readiness.md` — DEC-012 handoff package. Enumerates trademark / non-affiliation / license-text / third-party-attribution / `tsclientlib`-posture / crypto- export / data-handling items the legal reviewer must confirm, and lists the concrete engineering deliverables they block on (`cargo about generate`, `cargo deny check licenses`, Flutter `LicenseRegistry` dump). * `docs/governance/staged-release-plan.md` — DEC-002 channel schedule. Linux + Android sideload promote to GA on DEC-012 sign-off; Play Store / Windows / macOS / iOS gate on per- platform signed-build availability. Rollback policy included. * `product-decision-register.md` change-history advances to 0.9.8 with a single entry summarising v0.3, v0.4, and v1.0-rc.1 progress against DEC-001. No decision rows mutate. Build + ops ----------- * `NOTICE` refreshed for the MVP product-code dependency set: adds `chacha20poly1305`, `rand`, `zeroize`, `base64`, `keyring`, `connectivity_plus`, `path_provider`, `freezed_annotation`; drops PoC-only entries. * `CHANGELOG.md` restructured: explicit version sections for v0.3.0-beta.1, v0.4.0-beta.2, v1.0.0-rc.1. Previous "Unreleased" contents migrated into their respective milestone sections. * `.github/workflows/ci.yml` exports `CHANORA_DISABLE_KEYRING=1` for the cargo-test job — CI runners have no D-Bus session and the keyring crate would otherwise block. * `run-chanora.sh` reads `CHANORA_BUNDLE_FLAVOUR` (default `release`) and self-copies the latest cdylib into the bundle's `lib/` if missing. Verification ------------ * `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`: all green (49 unit tests across the workspace; up from 36 at v0.4.0-beta.2). * `cargo test -p chanora_core --release -- --ignored alpha_smoke` passes against the live `cn.teamspeak.app` (DNS → connect → snapshot → disconnect in ~2.5 s). * `flutter analyze`: clean. * `cargo build -p chanora_bridge --release` + `flutter build linux --release` produce a working Linux x86_64 bundle. No Android live test in this commit per the user's note that the physical device was removed; the Android arm64-v8a build path is mechanically identical to v0.4.0-beta.2. |
||
|
|
780fd7eca2 |
feat(beta): External Beta — passwords, channel join, mute, bookmarks, encrypted identity
The v0.3 client could only ever connect to a hardcoded default
channel with no password and offered no controls mid-call.
External Beta closes those gaps and tightens identity-at-rest.
User-facing additions
---------------------
* **Server password** on the connect form. Plumbed through
`BridgeError`-aware `connect(host, nickname, password)`. Empty
string means "no password" — no behaviour change for open
servers.
* **Channel join**: tapping a row (or its login icon) in the
channel tree issues a `client_move`. Names containing "🔒" or
"password" prompt for a channel password first.
* **Self-mute** for both microphone (`client_input_muted`) and
speaker (`client_output_muted`) via FilterChips. Output mute
also flips the audio engine's local output-muted flag so
playback silences immediately, before the server acknowledges.
* **Master output gain** slider (0–200%). Plumbed through an
`AtomicU32` (f32 bits) on the engine that the cpal output
callback multiplies into every sample.
* **Bookmarks**: SQLite-backed list with Save / Connect / Delete
actions. Bookmarks persist across app restarts; tapping one
pre-fills the form and dials immediately.
Hardening
---------
* **Encrypted identity at rest** (RISK-PoC-002 closure for the
file-only threat model). ChaCha20-Poly1305 envelope: nonce +
ciphertext written atomically with mode 0600; 32-byte DEK in a
separate `identity.dek` file. Legacy plaintext identity files
are auto-detected, read, and upgraded on the next save. Full OS-
keyring integration is still v0.4 work — documented in the
store's doc comment.
* **Mobile voice-comm routing**: on Android, `AudioEngine::start`
uses JNI to set `AudioManager.setMode(MODE_IN_COMMUNICATION)`
when `cfg.mobile_voice_preset` is true (default). This engages
the device-side AEC/NS pipeline on most Pixel/Moto/Samsung
hardware even though cpal still opens the AAudio default input
preset. Full `setInputPreset(VOICE_COMMUNICATION)` switch is
still RISK-AUDIO-MOBILE-001 (needs cpal upstream or an Oboe
fork).
* **Log noise**: bridge default `EnvFilter` now silences
`tsproto::resend=error` and `tsproto::packet_codec=error` so
the redacted diagnostic export is human-readable. Still
overridable via `RUST_LOG=...`.
Engineering
-----------
* **`chanora_storage`** gains `BookmarkRepository` (rusqlite
bundled) with `add` / `update` / `delete` / `list`. The
identity store now layers on `chacha20poly1305` + `rand` +
`zeroize` for the envelope.
* **`chanora_protocol`** exposes `move_to_channel` and
`set_muted` on `ProtocolClient`, dispatched through the
existing `connection_task` request channel onto tsclientlib's
generated `client.client_move(...)` and
`state.client_update().set_input_muted/set_output_muted(...)`
paths.
* **`chanora_core::ChanoraSession`** wires the bookmark store
next to the identity store inside `init_storage`, and adds
`list_bookmarks` / `add_bookmark` / `update_bookmark` /
`delete_bookmark` / `move_to_channel` / `set_self_muted` /
`set_output_gain`.
* **`chanora_audio::AudioEngine`** carries `output_gain` and
`output_muted` atomics; the output callback consults both. The
Android branch of `start()` engages MODE_IN_COMMUNICATION via
a small JNI helper that reuses the `ndk_context` global set by
the bridge's `android_init` hook.
* **`chanora_bridge::api`** adds `set_input_muted`,
`set_output_muted`, `set_output_gain`, `move_to_channel`,
`list_bookmarks`, `add_bookmark`, `update_bookmark`,
`delete_bookmark`, and the `BridgeBookmark` DTO. FRB v2.12
codegen regenerated.
Tests + CI
----------
* `chanora_storage` test count rises from 3 to 8 — bookmark CRUD
round-trip, missing-row → `NotFound`, encrypted round-trip
(verifies ciphertext is not the plaintext on disk), and the
legacy plaintext upgrade path.
* New `.github/workflows/ci.yml`: `cargo check --workspace`,
`cargo test --workspace --no-fail-fast`, `cargo clippy`
(advisory), `flutter analyze`, and `flutter test` excluding
the live-server `e2e` tag.
Live-verified on Moto G Stylus 5G against cn.teamspeak.app:
saved a bookmark, reconnected via it, joined a non-default
channel via tap, toggled both mutes, slid the volume, and the
redacted diagnostic export confirmed `AudioManager mode set to
MODE_IN_COMMUNICATION`, `client_move sent`, and `client_update
sent` lines.
|
||
|
|
fd181c014c |
feat(audio): A.5 — surface mobile voice-preset + effects toggles in AudioEngineConfig
The Beta scope for mobile DSP is OS-source-driven (Android `MediaRecorder.AudioSource.VOICE_COMMUNICATION`, iOS `AVAudioSession.Mode.voiceChat`) — letting the platform's built-in AEC / NS engage instead of shipping our own DSP chain on constrained devices. Linux desktop stays a deliberate no-op: PipeWire / ALSA's default source is correct for desktop voice and adding a software AEC there would regress against an already-good baseline. This commit lands the *config surface* through every layer: * `AudioEngineConfig` gains `effects: AudioEffects` (mirrors the DEC-007/008/009/010 toggles) and `mobile_voice_preset: bool` (default `true`). * On Android, `AudioEngine::start` logs the preset + effects requests so a future cpal / Oboe upstream switch can be observed via the redacted diagnostic export. * On iOS, the same log line documents the binding gap — Chanora iOS audio is documented-only for Beta per the release notes. * On Linux desktop, the flags are honoured by name but the engine continues to use the default ALSA / PipeWire source. No behaviour change. RISK-AUDIO-MOBILE-001 (new) tracks the actual preset switch. The follow-up work either pulls in an Oboe-based input host or waits for cpal upstream to expose `set_input_preset`. Either way the config flag is forward-compatible — callers do not need to change when the binding lands. |
||
|
|
43a3c9ba76 |
feat(events): A.4 — emit SnapshotChanged from the watchdog probe
Adds a new variant to the lifecycle event catalogue so the UI can
auto-refresh the channel/client tree without an independent polling
timer on the Dart side. The supervisor's existing 5 s snapshot probe
is the source of truth: it already pulls a full snapshot to keep
the watchdog honest, so we piggyback on it.
* `chanora_core::SessionEvent::SnapshotChanged { channels, clients }`
carries the latest channel and client counts.
* The supervisor compares the probe result to `last_counts` and
fires the event only when the count actually changes. `last_counts`
is reset to `None` on a successful reconnect so the freshly
dialled session re-emits its initial counts.
* `chanora_bridge::api::BridgeEvent::SnapshotChanged` is the
cross-bridge mirror.
* Flutter routes the event through `_onEvent`, which calls
`_onRefresh()` to repopulate the snapshot view.
The probe-driven detection has known limits — pure within-channel
client moves do not change the count and so are not surfaced. That
gap will close when the supervisor tracks a content hash in
addition to the count; the count-only signal is sufficient for the
common "someone joined / someone left" case observed on cn.teamspeak.app.
|
||
|
|
d2d9ba0a5b |
feat(diagnostics): A.3 — redacted in-memory log sink + user-initiated export
Replaces the diagnostics scaffold with the production redaction policy + a user-initiated export path that satisfies DEC-016 (no automatic uploads). * `chanora_diagnostics::Redactor` applies the six policy rules to every captured log line: `$HOME` paths → `[home]`; IPv4 + IPv6 literals → `[ip]`; email-shaped strings → `[email]`; long base64-ish tokens → `[token]`; substrings registered with `KnownSecretRegistry` → `[REDACTED]`. The registry implements SS-AUD-003 defence-in-depth: storage adapters can register secrets as they cross out of the keyring so an accidental `Debug` print is still scrubbed at write time. * `InMemoryLogSink` is a bounded ring buffer (cap 500 lines in the bridge) that always passes lines through the redactor before storing them. `RedactingLogLayer` plugs it into `tracing- subscriber` alongside the existing logcat / fmt layers. * `DiagnosticExport::from_sink` builds a plaintext blob — already redacted — combining free-form metadata (crate version, target os/arch) with the retained log tail. `bridge::api:: export_diagnostics()` is the Flutter-facing entrypoint (`#[frb(sync)]`). * `bridge_init` now installs the redaction layer on both Android and desktop hosts, switching from the global `fmt::init()` shortcut to a layered `Registry` so the in-memory sink can sit side-by-side with the platform sink. * Flutter adds a bug-report icon to the AppBar; tapping it opens a scrollable monospace dialog with Copy and Close actions. New `diagnosticsAction` / `copyAction` / `closeAction` strings land in `app_en.arb` + `app_zh.arb`. Tests cover the redaction matrix (IPv4, IPv6, email, long tokens, known secret), the ring buffer capacity, and the full `DiagnosticExport::to_text()` round-trip — 9/9 green. Live-verified on Moto G: the dialog rendered a multi-line transcript with `[ip]`, `[token]`, `[home]` substitutions, the metadata block showed `target_os=android` `target_arch=aarch64`, and Copy placed the same text on the clipboard. |
||
|
|
71ecb83781 |
feat(storage): A.2 — persist TS3 identity across app restarts
A fresh `Identity::create()` was generated on every connect, which meant the server saw a different client UID each time. Long-lived features (bookmarks, server-side bans, group membership) depend on a stable UID — restoring that now via a minimal directory-backed identity file. * `chanora_storage::IdentityFileStore` reads / writes a single `identity.tskey` file under a caller-supplied directory. On Unix the file is created with `O_CREAT | O_TRUNC | mode 0600`; on non-Unix targets the platform sandbox does the access control. Writes are atomic (temp file + `fsync` + `rename`) so a crash mid-write cannot leave a half-written identity on disk. Empty files are treated as "no identity" rather than as an error. * `chanora_protocol::ProtocolClient::generate_identity()` exposes the `counterVbase64key` serialisation used by tsclientlib's `Identity::new_from_str`, so the core layer can mint an identity and store it before dialling. * `chanora_core::ChanoraSession::init_storage(dir)` wires the store. `connect()` then resolves the identity in this order: (1) `cfg.identity` if explicitly supplied; (2) persisted value if any; (3) generate-and-persist a fresh one. * `chanora_bridge::api::init_storage(dir: String)` is the Flutter-facing entrypoint; the matching Dart side resolves `path_provider`'s `getApplicationSupportDirectory()` and calls it once on app start. * `BridgeError` now maps `CoreError::Storage`. Beta caveat (RISK-PoC-002 / SS-RISK-FALLBACK): the identity is not encrypted at rest. The v0.4 storage rework lands proper Secret Service + Android Keystore + iOS Keychain backends. Documented under `IdentityFileStore`'s doc comment. Live-verified on Moto G Stylus 5G: first connect generated + persisted the identity (visible in the redacted diagnostic export as "generated + persisted fresh identity"); disconnect + reconnect in the same session logged "reusing persisted identity" and dialled with the same UID. |
||
|
|
f52d702e27 |
feat(core): A.6.1 — use OS connectivity signals to drive reconnect
Extends the A.6 supervisor with an OS-level connectivity hint so a returning network triggers a redial immediately instead of waiting out the current backoff slot (up to 60 s). The watchdog remains the authoritative loss detector — the OS signal is advisory. * `chanora_core::NetworkState` (Unknown / Online / Offline) is owned by `ChanoraSession` via a `tokio::sync::watch::Sender`. `set_network_state()` / `network_state()` are the public accessors. * The supervisor's watch-phase `select!` gains a `network_rx` branch: Offline pre-charges watchdog misses (capped at `MAX_MISSES - 1`) so the next probe failure trips immediately; Online clears stale misses. This shrinks UI-banner latency on a Wi-Fi drop from ~15 s to ~5 s. * The reconnect-loop's backoff sleep races against Online: a transition cuts the sleep short and resets the attempt counter so future losses start at the smallest backoff window again. * `chanora_bridge` adds `BridgeNetworkState` (mirror enum) and a sync `set_network_state(state)` function. On platforms with no signal wired the supervisor stays at Unknown and falls back to pure watchdog/backoff — no behavioural regression vs A.6. * Flutter adds `connectivity_plus ^6.1.0` and wires `_wireConnectivity()` in `main()`: seeds with `checkConnectivity()` then forwards every `onConnectivityChanged` to the bridge, mapping any non-`none` transport to Online. Verified on Moto G Stylus 5G (Android 14): `svc wifi disable && svc data disable` for ~40 s — reconnect banner appeared promptly because the watchdog was pre-charged. After `svc wifi enable && svc data enable` the supervisor woke from its 15 s backoff slot and reconnected within seconds; the channel tree re-rendered without user action. |
||
|
|
0bef61aea2 |
feat(core): A.6 — supervisor reconnect with watchdog and event stream
Adds an end-to-end auto-reconnect path so a brief network outage no longer leaves the client wedged in a half-dead state. The flow has three layers, each motivated by a real failure mode observed on the Moto G live test: * `chanora_protocol::DisconnectReason` (`UserRequested` / `StreamEnded` / `Error(String)`) is reported on a `oneshot` when the per-connection task exits, so the supervisor can tell user intent apart from a real loss. * `chanora_core` spawns a supervisor task per `ChanoraSession`. It listens for the loss notifier AND runs a watchdog that issues `snapshot()` probes every 5s with a 4s timeout — three consecutive misses synthesise a `DisconnectReason::Error(...)` and trigger the reconnect path. The watchdog catches the "ghost connected" case where tsclientlib silently resets internal state but the event stream never errors. Backoff schedule: 1s, 2s, 5s, 15s, 30s, 60s (capped). On success the supervisor swaps the dead `ProtocolClient` for the new one in place and, if audio was running, restarts the audio engine bound to the new `voice_in`/`voice_out` channels. * `SessionEvent` (Connected / Lost / Reconnecting / Disconnected / AudioStarted / AudioStopped) is broadcast on a 64-slot channel. `chanora_bridge` re-exports it as `BridgeEvent` and exposes `events_stream(StreamSink)`; the Flutter side subscribes from `initState` and renders a reconnect banner with attempt count and delay. New `SnapshotProbe` exposes a clone-friendly snapshot path so the watchdog can probe without holding `&self` across awaits. Localization adds `statusReconnecting` and `statusConnectionLost` keys to `app_en.arb` and `app_zh.arb`. Verified on Moto G Stylus 5G (Android 14) against cn.teamspeak.app: killed Wi-Fi + cellular for ~70 s; watchdog declared loss at three misses, supervisor walked the backoff schedule, and the UI reconnected automatically once the radios came back. Snapshot tree re-rendered without user action. |
||
|
|
bc0da50cdb |
feat(protocol): A.1 — fix hostname resolution on Android and iOS
Resolves the Beta-blocking issue surfaced during Android v0.2.0-beta.1
verification: hostnames could not be used, only literal IPs.
Root cause:
tsclientlib's built-in resolver uses hickory-resolver, which reads
/etc/resolv.conf. That file does not exist on Android or iOS, so
any connect by hostname exited the connection task before
signalling ready and surfaced the cryptic error
BridgeError.connection(field0: protocol backend:
connection task exited before signalling ready)
Fix:
crates/chanora_protocol/src/resolver.rs (new):
Resolves hostnames via tokio::net::lookup_host, which uses the
platform's getaddrinfo. Works on every platform Chanora targets.
Tiny in-process positive-result cache (5 min TTL) keeps
reconnects cheap. IPv4 sorted ahead of IPv6 in the returned list
to favour the more reliable path on dual-stack networks.
crates/chanora_protocol/src/adapter.rs:
connection_task now resolves the hostname itself and passes the
resulting SocketAddr (not the hostname String) to
tsclientlib::Connection::build. tsclientlib's ServerAddress enum
accepts SocketAddr via its From impl, so the upstream resolver
is skipped entirely.
crates/chanora_protocol/src/lib.rs:
New typed error arm ProtocolError::DnsFailed { host, reason }
so the UI can distinguish 'server not found' from 'server
refused our packets'.
crates/chanora_bridge/src/lib.rs:
Matching BridgeError::DnsFailed { host, reason } DTO surfaced
to Dart, with explicit From<CoreError::Protocol(DnsFailed)>
mapping so the UI gets the structured fields rather than a
stringified mess.
Tests added (crates/chanora_protocol/src/resolver.rs::tests):
- rejects_empty
- literal_ipv4_short_circuits
- literal_ipv4_default_port_path
- unresolvable_returns_dns_failed
- resolves_known_hostname (#[ignore], --ignored to run; hits net)
Empirical verification (2026-05-14):
Workspace: cargo check + cargo test --workspace clean.
Live resolver test: cn.teamspeak.app → 175.178.125.23:9987 (passes).
cargo test -p chanora_core --test alpha_smoke -- --ignored:
server='Vigorous Pro' channels=42 clients=20 (passes by hostname).
flutter test: alpha_e2e_test + beta_e2e_test both green.
Physical Moto G Stylus 5G (Android 14 arm64-v8a):
APK rebuilt (48.9 MB). adb install + launch.
Connect form left at default 'cn.teamspeak.app'.
logcat shows:
chanora_protocol: dns resolved input=cn.teamspeak.app
resolved=175.178.125.23:9987
tsclientlib: starting connection to 175.178.125.23:9987
tsproto::resend: Connecting → Connected
chanora_protocol: initial state snapshot received
UI shows 'Connected to Vigorous Pro' / '42 channels • 20 online'.
This is the first item in Category A (post-Beta polish bundle).
Pause point: review before A.6 (full reconnect).
|
||
|
|
1324f478fe |
docs(release): add iOS build instructions + shell helper
The development host is Linux x86_64; the iOS toolchain (Xcode, xcrun,
codesign, iPhoneOS SDK) is macOS-only under Apple licence and cannot
be cross-compiled from Linux. This commit adds the instructions for
producing the iOS v0.2.0-beta.1 build on a macOS host plus a bash
helper that automates the build itself.
docs/release/ios-build.md (v0.1.0):
- Toolchain pin table (macOS 14+, Xcode 26+ per DEC-021, iOS SDK
26+, iOS deployment target 13.0 per DEC-003, Flutter 3.41.9,
Rust 1.95 stable with aarch64-apple-ios / aarch64-apple-ios-sim /
x86_64-apple-ios targets, CocoaPods 1.16+, FRB 2.12.0).
- macOS host options: owned hardware vs rental (MacStadium,
MacinCloud, Scaleway Apple silicon, AWS EC2 Mac) vs borrowed
Mac. Realistic cost ranges per option.
- Step-by-step Homebrew + Rust + Flutter + CocoaPods install.
- Pre-built libopus.a per arch via a CMake invocation that
targets the iOS SDK explicitly. Mirrors the Android build's
LIBOPUS_LIB_DIR wrap-dir trick.
- flutter create --platforms=ios to scaffold the ios/ folder
(the product Flutter app was created with only linux + android).
- Edits required to ios/Podfile and ios/Runner/Info.plist:
iOS 13 deployment target (DEC-003), NSMicrophoneUsageDescription
for the audio engine, UIBackgroundModes=audio for screen-locked
playback.
- Three cargo build --target invocations for device + both
simulator slices.
- lipo merge of the two simulator slices into one .a.
- xcodebuild -create-xcframework to produce
target/ChanoraBridge.xcframework with the right slices.
- flutter build ios --release --no-codesign or
flutter build ipa --release --export-method development for
a signed .ipa.
- Install paths: xcrun devicectl for wired install, altool for
TestFlight upload.
- Smoke-test instructions with the same hostname-resolution
caveat that affects the Android Beta (hickory-resolver does
not work on iOS; use the literal IP).
- Packaging into chanora-v0.2.0-beta.1-ios.ipa.
- Known-issue table covering: audiopus_sys cmake build failures
on iOS, microphone permission prompt prerequisites,
AVAudioSession category quirks for voice transmission, code-
signing failure modes, TestFlight rejection causes.
- Reproducibility note (build is not bit-reproducible).
tools/build-ios.sh:
- Parameter switches: --version, --no-codesign,
--regenerate-bindings, --export-method.
- Verifies xcodebuild, xcrun, cargo, rustc, flutter, pod, lipo
on PATH.
- Adds the three rustup iOS targets if missing.
- Verifies each pre-built libopus.a exists at the expected wrap
dir before starting.
- Optionally regenerates FRB bindings.
- Three cargo build runs (device + Apple-silicon sim + Intel
sim), each with LIBOPUS_LIB_DIR pointed at its arch's wrap dir
and the audiopus_sys build-cache wiped per target.
- lipo + xcodebuild -create-xcframework.
- flutter pub get + pod install + flutter build {ios,ipa}.
- Copies the .ipa to a versioned path under $HOME and prints
SHA-256.
This is documentation + helper only; no actual iOS binaries are
produced by this commit. The Linux development host cannot run
Xcode. To produce the binaries, follow §3-§14 of
docs/release/ios-build.md on a macOS host, or run
tools/build-ios.sh there.
DEC-011.1 iOS audio status remains Deferred; the doc notes that
cpal's iOS backend has not been empirically verified and the
AVAudioSession category likely needs configuration for voice
transmission. Both are Beta+ items.
|
||
|
|
c81ccfd9a9 |
feat(android): produce v0.2.0-beta.1 Android APK with voice in/out
Builds the Internal Beta product app for Android. Companion to the
Linux desktop build already shipped at the same tag.
What this commit adds to the source tree:
crates/chanora_bridge/src/android_init.rs (new):
JNI lifecycle for Android. JNI_OnLoad captures the JavaVM*.
Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext
is called by MainActivity.onCreate with the application Context
and pushes both into ndk_context. Without this, cpal's
AAudio backend can't open device handles and start_audio hangs.
crates/chanora_bridge/Cargo.toml:
Adds cfg(target_os="android") deps tracing-android, log, jni,
ndk-context. Linux/desktop builds are unaffected.
crates/chanora_bridge/src/lib.rs:
Conditionally includes the android_init module on Android.
crates/chanora_bridge/src/api.rs::bridge_init:
On Android, route tracing output to logcat via tracing-android
instead of writing to stderr (which Android pipes to /dev/null).
Logs show under `adb logcat -s chanora`.
apps/chanora_flutter/android/app/src/main/AndroidManifest.xml:
Adds uses-permission android.permission.INTERNET (needed for
the protocol layer) and android.permission.RECORD_AUDIO (needed
by chanora_audio's capture stream). Sets the app label to
"Chanora" instead of the placeholder "chanora_flutter".
apps/chanora_flutter/android/app/src/main/kotlin/.../MainActivity.kt:
Overrides the Flutter-generated MainActivity. Loads
libchanora_bridge.so eagerly at class-init so JNI_OnLoad runs
before any FRB call. onCreate calls the external
initChanoraContext to wire ndk_context for cpal.
apps/chanora_flutter/pubspec.yaml:
Bumps version 1.0.0+1 → 0.2.0+2 to match the v0.2.0-beta.1 tag.
run-chanora.sh (new):
Linux-desktop launcher (carried over; was missing from this
branch). Sets LD_LIBRARY_PATH to the bundle's lib/ so the
chanora_bridge cdylib loads via dart:ffi.
Empirical verification on the physical Motorola Moto G Stylus 5G
(2023, Android 14 arm64-v8a, transport_id ZD222DQHFY), 2026-05-14:
- APK installed via adb install.
- Activity launched; permissions granted.
- Connect form filled with 175.178.125.23 (Vigorous Pro's IP —
see honest limitation below); Connect button tapped.
- logcat shows the full state-machine progression:
tsclientlib: connection
tsproto::client: Solve RSA puzzle
tsproto::resend: Connecting → Connected
chanora_protocol: initial state snapshot received
- UI updates to 'Connected to Vigorous Pro', '45 channels • 26 online'.
- Welcome banner with CJK characters preserved verbatim.
- 'Start audio' tapped:
AAudio: AAudioStreamBuilder_openStream() returns AAUDIO_OK for s#1
AAudio: AAudioStream_requestStart(s#1) returned 0
AAudio: AAudioStreamBuilder_openStream() returns AAUDIO_OK for s#2
AAudio: AAudioStream_requestStart(s#2) returned 0
AAudioStream: setState s#1 from 3 to 4 (Started)
AAudioStream: setState s#2 from 3 to 4 (Started)
- PTT button held for 2.5 s:
UI shows: 'TX 124 frames • RX 0 frames • PTT off'.
124 frames / 2.5 s ≈ 50 frames/s = 20 ms Opus frames — exactly
the encoder cadence. Voice transmission proven over UDP to the
real server.
Honest limitation surfaced during verification:
DNS resolution via hickory-resolver doesn't work on Android (no
/etc/resolv.conf). Connecting by hostname produces:
BridgeError.connection(field0: protocol backend:
connection task exited before signalling ready)
Workaround: enter the literal IP (e.g. 175.178.125.23 for
cn.teamspeak.app). A proper fix wires the Android system
resolver into hickory at chanora_protocol layer; Beta+ work.
Build prerequisites (documented for reproducibility):
- Android NDK r26.3.11579264 at /opt/android-sdk/ndk/26.3.11579264.
- rustup targets: aarch64-linux-android, armv7-linux-androideabi,
x86_64-linux-android.
- cargo-ndk 4.x.
- Pre-built libopus.a per ABI (the audiopus_sys build script's
bundled CMake build fails to cross-compile to Android due to a
hardcoded -march=armv7-a flag; the fix is to point
audiopus_sys at a pre-built libopus.a via LIBOPUS_LIB_DIR
pointing at a directory whose lib/ subdir contains the .a).
Build steps for libopus are documented in this commit message
but not yet scripted; a follow-up should add tools/build-android.sh.
- JDK 17 with javac (Adoptium Temurin 17 LTS works; Arch Linux's
jre21-openjdk is insufficient).
ABIs built and shipped in the APK:
arm64-v8a, armeabi-v7a, x86_64.
Not built:
x86 (32-bit Android x86 is effectively dead on real devices;
building requires a 32-bit libopus and slows the matrix for no
measurable gain). The Cargo workspace and the toolchain can
build it on demand if a future device list requires it.
|
||
|
|
8094ec7277 |
docs(release): add Windows build instructions + PowerShell helper
The development host is Linux x86_64; `flutter build windows` cannot
be cross-compiled and requires a Windows host with Visual Studio
2022's C++ Desktop workload. This commit adds the instructions for
producing the Windows v0.2.0-beta.1 Internal Beta build on an Azure
VM, plus a PowerShell helper that automates the build itself.
docs/release/windows-build.md (v0.1.0):
- Toolchain pin table (Windows Server 2022, VS 2022 Build Tools
+ C++ workload, Flutter 3.41.9, Rust 1.95 stable, FRB 2.12.0,
CMake, audiopus build dependency).
- Azure VM provisioning recipe: Standard_D4s_v5 (4 vCPU / 16 GiB),
Premium SSD 128 GiB, RDP locked to caller IP, auto-shutdown,
cost estimate (<USD 1 per build session).
- Step-by-step PowerShell to install VS 2022 Build Tools with the
required components, Git for Windows, rustup, Flutter SDK,
flutter_rust_bridge_codegen, and CMake.
- Two upload paths for source: temporary git remote OR zip archive
over RDP clipboard.
- flutter create --platforms=windows to scaffold the
windows/ platform folder (the product Flutter app was created
with only linux + android).
- cargo build -p chanora_bridge to produce chanora_bridge.dll.
- flutter build windows --release to produce chanora_flutter.exe
and the bundle.
- Drop the DLL next to the EXE so dart:ffi loads it.
- Smoke-test instructions including the cn.teamspeak.app UDP 9987
egress gotcha for some Azure regions.
- Packaging into chanora-v0.2.0-beta.1-windows-x64.zip.
- Known-issue / caveat table.
- Reproducibility note (build is not bit-reproducible in this Beta).
tools/build-windows.ps1:
- Parameter switches: -SkipRustBuild, -RegenerateBindings, -Version.
- Verifies flutter, cargo, rustc, cmake, git on PATH.
- Adds the x86_64-pc-windows-msvc target via rustup if missing.
- Runs flutter create --platforms=windows if the windows/ folder
is absent in apps/chanora_flutter/.
- Optionally regenerates FRB bindings.
- cargo build --release -p chanora_bridge --target x86_64-pc-windows-msvc.
- flutter pub get + flutter build windows --release.
- Copies the DLL into the Release bundle.
- Compress-Archive into chanora-<version>-windows-x64.zip.
- Prints final artefact paths.
This is documentation + helper only; no actual Windows binaries are
produced by this commit. To produce the binaries, follow §3-§13 of
docs/release/windows-build.md on a Windows host.
|
||
|
|
9790005c3e |
feat(beta): wire voice in/out end-to-end with push-to-talk (v0.2.0-beta.1)
Reaches the Internal Beta milestone of DEC-001's release sequence the
same day as Alpha. Adds voice capture and playback through the full
Flutter UI → FRB → Rust core → tsclientlib → server path.
Promotions from PoC:
poc/audio-capture-playback-spike → crates/chanora_audio/
New product code:
crates/chanora_audio/src/engine.rs — cpal capture and playback,
audiopus Opus VoIP encoder (48 kHz mono 20 ms frames), tsclientlib
AudioHandler for decode + jitter buffer + mix on playback,
push-to-talk gate, graceful playback-only fallback when capture
is unavailable.
crates/chanora_protocol/src/adapter.rs — extended with
voice_out_tx (clonable mpsc::Sender<OutPacket>) and
take_voice_in() (one-shot mpsc::Receiver<InboundVoice>); main
loop now interleaves outbound voice drain, event pumping, and
control-request handling.
crates/chanora_protocol/src/lib.rs — re-exports the few
tsproto_packets types (OutAudio, OutPacket, InAudioBuf,
AudioData, CodecType, Direction) that chanora_audio
legitimately needs. Documented as the single deliberate
cross-crate type re-export per SAD-067, justified by the
performance cost of a parallel type hierarchy on the 20 ms
voice frame.
core/chanora_core/src/lib.rs — ChanoraSession::start_audio,
set_ptt, audio_stats; disconnect now stops the engine first.
crates/chanora_bridge/src/api.rs — startAudio, setPtt,
audioStats commands and BridgeAudioStats DTO.
apps/chanora_flutter/lib/main.dart — "Start audio" button +
hold-to-talk PTT button with pressed/released visual state +
live stats line (TX/RX/PTT). Stats polled every 500 ms.
ARB:
Both en and zh-Hans gain startAudioAction, pttHoldToTalk,
pttTransmitting, audioStatsLine. Banner updated to
"Beta build — voice in/out wired; not production ready."
FRB config:
flutter_rust_bridge.yaml gains local: true so codegen resolves
the workspace member's library stem to "chanora_bridge" instead
of falling back to "UNKNOWN".
Empirical verification (2026-05-14, against cn.teamspeak.app):
cargo check + cargo test --workspace: all green.
flutter analyze: 0 issues.
flutter test: 4/4 passing including:
- test/alpha_e2e_test.dart (regression: Alpha still works)
- test/beta_e2e_test.dart (Beta: connect → startAudio →
PTT cycle → disconnect against cn.teamspeak.app).
Live smoke (cargo test alpha_smoke -- --ignored): 49 channels,
37 clients retrieved.
Capture stream open against the host PipeWire auto_null source
refused (snd_pcm_hw_params); engine correctly logged the warning
and continued in playback-only mode. TX=0 frames, RX=0 frames
reflects the headless null-source environment; on a real mic
host the encoder produces ~50 frames/second while PTT is held.
Honest Beta scope (NOT in this release):
- AEC / AGC / NS / HPF DSP (DEC-007..010): AudioEffects exists
as a struct but the filters are no-ops. Beta+ work.
- Production-quality resampler: current code is linear
interpolation. Beta+ work.
- Identity persistence via chanora_storage: still ephemeral.
- Push-to-Dart event stream: UI polls instead.
- chanora_diagnostics tracing-layer wiring: still scaffold.
- Mobile (Android) cdylib + UI: PoC-proven, not yet in product.
- Reconnect / network-loss recovery for the voice path.
Docs updates:
- docs/governance/product-decision-register.md bumped to v0.9.7
(Beta-milestone change-history entry; no row changes).
- docs/governance/poc-results-summary.md bumped to v0.6.0
(RISK-PoC-005 updated with Beta progress).
v0.2.0-beta.1
|
||
|
|
53b176b722 |
docs(governance): record Alpha milestone in PoC results summary (v0.5.0)
Updates RISK-PoC-005 to 'partially closed' and adds a v0.5.0 change
history entry documenting:
- the tsclientlib spike promotion into crates/chanora_protocol;
- the typed ChanoraSession in core/chanora_core wiring the
protocol API;
- the FRB 2.12.0 bridge wiring;
- the Flutter Alpha UI;
- the empirical verification path through alpha_e2e_test.dart
and alpha_smoke.rs;
- the v0.1.0-alpha.1 tag pointing at commit 3bb038c.
No other doc bumps are needed: the decision register stays at v0.9.6
(no new decisions), the audit reports stay at v0.9.3 (no new audit
evidence beyond what the PoCs already provided), the PoC plan stays
at v0.3.0 (all six PoC entries were already PASS).
|
||
|
|
4915ec0a1b |
feat(alpha): wire connect→snapshot→disconnect end-to-end (v0.1.0-alpha.1)
First Internal Alpha build per DEC-001. Closes the milestone of
'Flutter UI calls Rust via the typed bridge, Rust connects to a
TeamSpeak-compatible server through tsclientlib, returns a typed
snapshot, and disconnects cleanly.' Audio remains Beta scope.
Promotions from PoC:
poc/tsclientlib-connect-spike → crates/chanora_protocol/
New product code:
crates/chanora_protocol/src/{dto.rs,adapter.rs} — typed boundary
over tsclientlib. Tokio task owns the Connection; public
handle communicates via mpsc/oneshot. No tsclientlib types
cross out of the crate (SAD-067 / SysDes-011 / SysDes-029).
core/chanora_core/src/lib.rs — ChanoraSession composes the
protocol crate, enforces the DEC-006 single-connection
invariant.
crates/chanora_bridge/src/{api.rs,frb_generated.rs} — FRB 2.12.0
bridge per DEC-014. cdylib + staticlib + rlib. Typed
BridgeSnapshot / BridgeChannel / BridgeClient / BridgeError
DTOs. Process-wide OnceLock<Runtime> + OnceLock<ChanoraSession>.
flutter_rust_bridge.yaml at repo root.
apps/chanora_flutter/lib/main.dart — Alpha UI: server form,
connect button, channel tree, disconnect.
apps/chanora_flutter/lib/l10n/app_{en,zh}.arb expanded with the
Alpha key set; ARB metadata reaffirms ADR-008 for
server-provided content.
Generated Dart bindings under apps/chanora_flutter/lib/src/rust/.
Empirical verification (2026-05-14):
Workspace: cargo check + cargo test clean
(workspace tests: all green).
Bridge cdylib: target/release/libchanora_bridge.so produced
(~15 MB).
Flutter: flutter analyze clean; flutter test runs 3/3 green
including the alpha_e2e_test that drives the full
Dart → FRB → chanora_bridge → chanora_core → chanora_protocol
→ tsclientlib → UDP → cn.teamspeak.app
path. The captured logcat/stdout shows the tsproto resender
transitioning Connected → Disconnecting → Disconnected on
clean teardown.
Architecture changes:
- Removed the chanora_core ↔ chanora_bridge cyclic dependency.
chanora_core no longer knows the bridge exists; the bridge
maps from CoreError.
- chanora_bridge crate's #![forbid(unsafe_code)] lint relaxed
because FRB-generated glue legitimately uses unsafe at the
FFI boundary. Hand-written code remains unsafe-free.
Open follow-ups (NOT in this Alpha):
- Audio capture/playback wiring into chanora_audio
(Beta scope per DEC-001).
- Identity persistence via chanora_storage
(currently regenerated on every connect).
- Per-message diagnostics + redaction
(chanora_diagnostics still scaffold).
- Reconnect / network-loss recovery.
- Mobile (Android) build of the bridge cdylib + UI verification.
v0.1.0-alpha.1
|
||
|
|
e0f34009d9 |
docs(governance): record product scaffolding paths (DEC-022)
Updates two documents to reflect the workspace + Flutter app
scaffold landed in the previous commit.
path-migration-map.md v0.9.2 -> v0.9.3:
Adds §3 Implementation Path Layout. Lists each subsystem's
canonical crate path alongside its SAD / SysDes / DEC authority.
Notes that the Flutter app is owned by Flutter tooling and is
not a Cargo workspace member.
CHANGELOG entry under [Unreleased]:
- Documents the seven new Cargo workspace members and the
invariants pinned at the workspace level.
- Documents the Flutter app scaffold, the DEC-004 minSdk = 28
override, and the DEC-015 English + Simplified Chinese ARB
catalogue (with the ADR-008 server-content reaffirmation).
- Records the empirical verification (cargo check + cargo test
+ flutter analyze + flutter test all clean).
|
||
|
|
974dda9601 |
feat(scaffold): create product workspace + Rust crates + Flutter app
Implements the canonical implementation directory layout adopted by
DEC-022 (register v0.9.5). Closes the scaffolding phase; no PoC code
has been promoted in yet (per proof-of-concept-plan.md §4 a PoC is
not product code unless explicitly promoted).
Rust workspace
==============
Top-level Cargo.toml declares seven workspace members:
core/chanora_core top-level Rust API + orchestration
crates/chanora_protocol tsclientlib isolation (SAD-067, SysDes-011/029)
crates/chanora_state state sync, reducers, deltas
crates/chanora_audio capture, DSP, Opus, jitter, mixer, playback
crates/chanora_storage non-secret DB + platform secure store
crates/chanora_diagnostics logs, redaction, export
crates/chanora_bridge typed Flutter/Rust DTOs
Workspace-wide pins:
license = "MIT OR Apache-2.0" (DEC-020)
rust-version = "1.95"
edition = "2021"
The Flutter app (apps/chanora_flutter) is NOT a Cargo workspace
member; it is owned by the Flutter / Gradle toolchain and is in the
workspace exclude array along with every poc/* spike.
Each crate ships:
* a Cargo.toml referring to workspace.dependencies pins;
* a lib.rs with #![forbid(unsafe_code)] + #![warn(missing_docs)],
a typed Error enum, and the public types relevant to the
subsystem's role per SAD §7.2;
* minimal unit tests so
running 1 test
test tests::defaults_match_decisions ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::it_compiles ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::session_can_be_constructed ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::marker_matches_poc ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::it_compiles ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::state_transitions_compile ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::it_compiles ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s is non-empty.
chanora_core's CoreError type-wraps every subsystem error via
#[from] so callers can match on origin without parsing strings.
chanora_audio's AudioEffects struct defaults all four effects to
true, matching DEC-007 (AEC), DEC-008 (AGC), DEC-009 (NS),
DEC-010 (HPF). A unit test pins this so a future regression that
flips a default fails immediately.
chanora_diagnostics exports REDACTION_MARKER = "[REDACTED]",
identical to the PoC's marker so audit grep patterns survive the
promotion.
chanora_bridge's BridgeError is Serialize + Deserialize so it can
flow across the FRB 2.x boundary (DEC-014).
Empirical verification: cargo check --workspace clean, cargo test
--workspace clean (7 unit tests + 7 doc-test runners, all passing)
against Rust 1.95.0 stable.
Flutter app
===========
apps/chanora_flutter created with .
DEC-004 applied: minSdk overridden to 28 in
android/app/build.gradle.kts with a comment that points back at the
decision register and forbids lowering it without re-opening DEC-004.
DEC-015 applied: shipped English + Simplified Chinese at MVP.
- pubspec.yaml gains flutter_localizations + intl + generate:true.
- l10n.yaml emits lib/l10n/generated/AppL10n (no synthetic package
— that was removed in Flutter 3.41+).
- lib/l10n/app_en.arb is the source of truth; lib/l10n/app_zh.arb
mirrors the key set in zh-Hans. ARB metadata explicitly
reaffirms ADR-008: server-provided content (channel names,
nicknames, welcome banners) is preserved verbatim and never
translated.
lib/main.dart and test/widget_test.dart were rewritten from the
template counter into a minimal localized scaffold that proves
both locales render correctly.
Empirical verification: reports no issues;
runs the two locale smoke tests and both pass.
|
||
|
|
0f418f1d7e |
feat(legal): resolve DEC-020 — dual-license under Apache-2.0 OR MIT
Closes the only previously-open decision in the register. Chanora is
now dual-licensed under either:
* Apache License, Version 2.0 (LICENSE-APACHE), OR
* MIT License (LICENSE-MIT)
at the recipient's option. This is the standard Rust-ecosystem
permissive model and is compatible with every direct dependency
in the PoC tree:
tsclientlib MIT OR Apache-2.0
flutter_rust_bridge MIT
cpal Apache-2.0
rusqlite MIT
keyring MIT OR Apache-2.0
hound Apache-2.0
ndk-context, jni, android_logger, regex, serde, tokio,
tracing, thiserror, zeroize, etc. MIT OR Apache-2.0
and with the Flutter framework's BSD-3-Clause.
Files added:
- LICENSE-APACHE Apache 2.0 license text.
- LICENSE-MIT MIT license text with the standard 2026 copyright
line.
Files updated:
- LICENSE Now the dual-license aggregator. Includes the standard
Apache-2.0 inbound-contribution clause ("Unless you
explicitly state otherwise, any contribution
intentionally submitted for inclusion in Chanora by you,
as defined in the Apache-2.0 license, shall be
dual-licensed as above, without any additional terms or
conditions.").
- NOTICE Rewritten with the dual-license declaration and an
inventory of direct dependencies with their upstream
licenses. Transitive deps remain to be enumerated by
build tooling (cargo about, Flutter LicenseRegistry).
- README.md §License section rewritten to point at LICENSE-APACHE
and LICENSE-MIT.
- docs/governance/product-decision-register.md v0.9.5 → v0.9.6:
DEC-020 status: Open → Accepted. §4 license row updated. §6
collapsed: every previously-Proposed or Open decision in the
register is now resolved. DEC-012 legal review remains as a
release-gating *work* item, but is not an open decision.
- docs/governance/poc-results-summary.md v0.3.0 → v0.4.0:
RISK-PoC-003 closed. DEC-020 row moved out of 'Still open'.
This is a license-model commitment, not a substitute for the
DEC-012 legal review. Per DEC-012 the actual legal review work
(transitive-dep OSS obligations, trademark registrability, final
sign-off on the non-affiliation wording) must still be completed
before any public/store release; that is sign-off work, not an
architectural decision.
Decision register state after this commit:
Accepted: 23 of 23 unique decisions
Open/Deferred: 0
Proposed: 0
|
||
|
|
a0d1c35461 |
docs(governance): owner confirmation on remaining 17 decisions (register v0.9.5)
Closes the 'Proposed / Owner Confirmation Required' state for every
decision in the register except DEC-020 (license, explicitly deferred
and now the only public-release-gating decision outstanding).
Accepted as recommended:
DEC-001 (Alpha → Beta → Public release sequence),
DEC-002 (all five platforms as MVP target, staged release allowed),
DEC-003 (iOS minimum: iOS 13),
DEC-005 (Android target SDK: Play-required API on upload date),
DEC-006 (single active server connection in MVP),
DEC-007/008/009/010 (AEC + AGC + NS + HPF defaults),
DEC-011 (platform-native audio first),
DEC-012 (legal/trademark/licensing review as a release gate),
DEC-013 (SQLite or equivalent for non-secret state),
DEC-016 (no automatic diagnostics upload),
DEC-017 (crash reporting disabled for MVP),
DEC-018 (product name: Chanora),
DEC-019 (drafted non-affiliation wording),
DEC-021 (Apple App Store SDK gate: Xcode 26+ / iOS 26 SDK+ on/after 2026-04-28).
Modified from the original recommendation by explicit owner ruling:
- DEC-004: Android minimum raised to API 28 (Android 9.0) from the
recommended API 24. Rationale: simpler audio path (AAudio stable
from API 28), narrower compatibility / privacy / scoped-storage
surface. Affects the Android spike's minSdk=24 in product code:
apps/chanora_flutter will need minSdk=28.
- DEC-015: MVP product language expanded to English + Chinese
(Simplified) from the recommended English-only. Rationale: the
demonstrated TS3-compatible-server audience (verified live against
cn.teamspeak.app) and broader TS3 audience include substantial
Chinese-speaking users. Adds zh-Hans translation, font, and
text-length-budget work to MVP. Server-provided content is still
preserved verbatim per ADR-008.
Still Open / Deferred:
- DEC-020 license model. The only remaining release-gating decision.
Documentation updates:
- product-decision-register.md → v0.9.5. §3 statuses updated, §4
renamed Recommended → Accepted with MODIFIED rows annotated,
§6 collapsed to DEC-020 only, §7 dated and statused for every
decision, change-history entry added.
- poc-results-summary.md → v0.3.0. §4 expanded with the
2026-05-14 owner-confirmation pass table. RISK-PoC-004 closed.
New RISK-PoC-006 (Android minSdk move 24 → 28) and RISK-PoC-007
(MVP language expansion to en + zh-Hans) added.
- CHANGELOG entry under [Unreleased].
|
||
|
|
4c64517e45 |
docs(governance): promote audio PoC to PASS; close mobile-Android half
Documentation update following the Android audio spike pass.
Decision register (v0.9.3 → v0.9.4):
- DEC-011.1 promoted from
'Accepted (desktop: cpal) / Deferred (mobile)'
to
'Accepted (desktop: cpal; Android: cpal-on-Oboe) / Deferred (iOS)'.
- Evidence pointer added: poc/audio-capture-playback-android-spike/
VERIFICATION.md.
PoC plan (v0.2.0 → v0.3.0):
- Audio row promoted from PARTIAL PASS to PASS.
- All six PoC plan entries are now PASS.
PoC results summary (v0.1.0 → v0.2.0):
- Audio row collapsed into one PASS spanning both spikes.
- RISK-PoC-001 narrowed from 'mobile audio' to 'iOS audio only'.
- Toolchain table expanded with Android NDK, cargo-ndk, AGP/
Gradle/Kotlin, jni/ndk-context/android_logger, and the test
device.
Cross-spike pointers updated:
- poc/audio-capture-playback-spike/VERIFICATION.md result and
follow-up sections updated to reference the Android spike.
- poc/README.md status table lists both audio spike directories.
CHANGELOG updated under [Unreleased].
|
||
|
|
ec21a880d2 |
feat(poc/audio): add Android mobile audio spike
Closes the mobile half of the PoC plan §2 audio exit criterion
left open by poc/audio-capture-playback-spike. The desktop and
mobile halves together fully retire the audio PoC.
Stack:
Kotlin (MainActivity) → JNI → Rust cdylib
→ cpal 0.16 → Oboe (AAudio / OpenSL ES) → Android audio HAL
Layout:
rust crate (src/lib.rs) — JNI_OnLoad, initContext,
playSine440, record1sToFile;
panic-catching at JNI boundary;
android_logger → logcat
android/ (Gradle 8.7, — minSdk 24, compileSdk 34, AGP 8.5.2.
AGP 8.5.2, Kotlin 1.9.24) cargoBuildRust task wraps cargo-ndk
-P 26 -t <abi> for all four ABIs;
wired into preBuild so AGP picks up
the produced .so files.
Verified on 2026-05-13 on a physical Motorola Moto G Stylus 5G
(2023), Android 14 SDK 34 arm64-v8a:
- Playback: 500 ms 440 Hz mono sine, 22,050 frames emitted at
44.1 kHz through cpal/Oboe/AAudio/device speaker.
- Capture: 1 s from default input, 42,624 frames written to
/data/data/app.chanora.poc.audio/files/chanora_poc_capture.wav.
File pulled via 'adb exec-out run-as ... cat' and confirmed
by file(1) as 'RIFF (little-endian) data, WAVE audio,
Microsoft PCM, 16 bit, mono 44100 Hz'. Header bytes
cross-checked against the reported frame count.
Notes:
- cpal links libaaudio (introduced API 26), so cargo-ndk targets
API 26 via -P 26 while the Android module's minSdk stays at 24
(DEC-004). API 24/25 devices would fall back to OpenSL ES at
runtime; not exercised here.
- JNI panic safety: every JNI entry point wraps its body in
std::panic::catch_unwind and a tracing panic hook routes
panic messages to logcat under tag 'ChanoraAudioPoC'. Without
this, cpal panicking inside an extern "system" function would
abort the process.
- The emulator AVD chanora-poc-api34 and its system image were
installed during Phase 0 but emulator verification was skipped
once the physical-device run succeeded. Real-device evidence
is stronger.
Surfaced finding: DEC-011.1 mobile half promoted from Deferred to
Accepted for Android in the same docs commit; iOS remains
explicitly Deferred (requires macOS + Xcode hardware).
Authority: PoC plan §2, DEC-011, DEC-011.1.
Not product code; not promoted into chanora_audio.
|
||
|
|
eca93a141e |
build(repo): gitignore Android/Gradle build artifacts
Adds .gradle/, local.properties, and **/jniLibs/**/*.so to the ignore list. The jniLibs .so files (600 KB - 950 KB per ABI) are produced by the cargoBuildRust Gradle task wrapping cargo-ndk; they are regenerated on every build and must not be tracked. Required by the next commit (the Android audio spike) which otherwise would try to track those four prebuilt libraries. |
||
|
|
271d23faf7 |
docs(governance): record PoC outcomes, owner decisions, and audit evidence
Closes Phases A and D of the post-PoC sequencing.
Decision register (v0.9.2 → v0.9.3):
- DEC-014 Accepted: flutter_rust_bridge 2.x pinned (closed by
poc/flutter_rust_bridge_hello).
- DEC-013.1 Accepted: rusqlite (bundled) (closed by
poc/sqlite-storage-spike).
- DEC-013.2 Accepted: Linux secure-storage backend policy —
Secret Service preferred, keyutils fallback (closed by
poc/secure-storage-spike; resolves SysRS-053 / SysRS-162
ambiguity).
- DEC-011.1 Accepted (desktop: cpal) / Deferred (mobile)
(closed by poc/audio-capture-playback-spike desktop half only).
- DEC-022 Accepted: canonical implementation directory layout per
the README sketch and SAD §7.2.
- DEC-020 explicitly Deferred by owner; remains a public-release
blocker.
Audit reports updated with empirical evidence:
- docs/security/secure-storage-audit-report.md v0.9.3:
SS-AUD-001/002/003/005/006 = PoC Pass with evidence pointers;
SS-TC-003 (Linux) Actual Result populated and Status = PoC Pass;
SS-AUD-004 cross-referenced to diagnostics-redaction PoC;
findings SS-FIND-001 (closed by DEC-013.2), SS-FIND-002 (keyutils
session caveat), SS-FIND-003 (non-Linux adapters still open).
- docs/security/diagnostic-redaction-audit-report.md v0.9.3:
REDACT-TC-001..010 = PoC Pass with evidence pointers; export
bundle policy §5 populated for every row; findings
REDACT-FIND-001 (regex coverage), REDACT-FIND-002
(tracing-layer integration), REDACT-FIND-003 (cross-spike
KnownSecretRegistry contract).
PoC plan (v0.1.0 → v0.2.0):
- Status column added to §2; outcomes recorded.
New doc:
- docs/governance/poc-results-summary.md v0.1.0 — single-page
reviewer-facing summary listing each spike's status, the
toolchain exercised, the owner decisions taken, the audit
coverage table, and open risks RISK-PoC-001..005 (mobile audio,
non-Linux secure-storage adapters, license, remaining
Proposed decisions, no product code yet).
This completes the post-PoC documentation work. Repo is at a clean
pause point: PoC code is committed, owner decisions are recorded,
audit reports carry empirical evidence, and the residual risks are
named in the summary doc.
|