d83539436e6c5f624fdd419375fc23fbbb40c6a1
289
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a93109ac37 |
fix(ios): rebuild chanora_bridge.framework on every Xcode build, not just pod install
The podspec's prepare_command only fires on `pod install`. Once a framework was generated, Rust source changes were silently ignored because Xcode kept re-bundling the stale framework into Runner.app. Manifested today as: ran `cargo build --release --target aarch64-apple-ios` to pick up the lenient voice_join fix, ran `flutter build ios`, but Runner.app/Frameworks/chanora_bridge. framework/chanora_bridge was still the framework from the previous pod install (16:10) not the just-built 18:30 dylib. Add an explicit `script_phase` to the podspec that re-runs: 1. cargo build --release --target aarch64-apple-ios -p chanora_bridge 2. cp dylib into Frameworks/chanora_bridge.framework/chanora_bridge 3. install_name_tool -id @rpath/... on every Xcode 'Build', not just on pod install. The script short- circuits when the framework's binary mtime is newer than the cargo output (fast no-op on incremental builds where Rust didn't change). Side effect: every Xcode build now invokes cargo, which can take ~5 s on a warm cache and ~1 min cold. This is the right trade-off because the previous behavior silently shipped stale Rust code. |
||
|
|
f1f81a3d7e |
fix(core,bridge,ios): voice_join survives audio-engine failure; iOS log file
User report from iPhone: 'mute / continuous / PTT buttons missing'
with NO error popup. Root cause: voice_join's audio-engine startup
was failing silently, and the failure propagated out as a hard
error \u2014 which means SessionEvent::VoiceState(true) was never sent
to Dart even though the server-side channel move had already
succeeded. Dart's _inChannel stayed false; every control gated on
_inChannel disappeared while the channel tree continued to show
the user as joined.
This commit makes voice_join lenient on audio-engine failures so
the UI state matches the server-side reality, and also gives iOS
a writable log file so diagnostics from device builds are
recoverable for the first time.
core/chanora_core/src/lib.rs::voice_join
* ensure_audio_running's error is now logged + emitted as
SessionEvent::AudioStopped, but does NOT abort voice_join.
The server move at step 1 already succeeded; failing the
Dart-visible promise here would leave the UI in a phantom
'in-channel visually but no controls' state. After this
commit:
- mic / headset / settings appear in the AppBar
- PTT button appears at the bottom
- status chip shows live audio state ('Mic on/off')
- if audio actually failed (mic permission denied,
no input device, CoreAudio rejecting stream config)
the user can retry by switching modes / channels;
BridgeEvent::AudioStopped wires _audioStarted=false
in Dart so audio-stats poll is honest about the
engine state.
crates/chanora_bridge/src/api.rs::log_file_path
* iOS now writes the log to /home/milkice/Documents/chanora.log
(Documents is the standard user-visible iOS sandbox dir).
* Android remains None pending the bridge JNI init wiring
a writable path (P1 follow-up).
apps/chanora_flutter/ios/Runner/Info.plist
* Adds UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace
so the Documents directory shows up under 'On My iPhone \u2192
Chanora' in the Files.app. The user can now copy chanora.log
out for support without needing Xcode \u2192 Devices and
Simulators \u2192 Download Container.
Workspace tests: 78/0/1 unchanged.
flutter build ios --release --no-codesign: 22.7 s clean
(Runner.app 29.9 MB).
|
||
|
|
3b08ea9d32 |
feat(ui,mobile): Plan E voice UI -- status chip + bottom-anchored PTT + AppBar mutes
Restructure the narrow / mobile body layout around three principles
(Hoober thumb-zone research validated, Material 3 components):
1. Channel tree gets ~85% of the screen height.
2. Live voice state is always visible in a 2-line status chip
above the PTT button.
3. PTT button is wide, bottom-anchored (thumb-natural lower zone).
4. Frequent toggles (mic mute, headset mute, voice settings) live
in the AppBar so they don't compete with the channel tree.
5. Non-essential live readouts (level meter, TX/RX, capability
badge) live in a modal sheet opened by tapping the status chip
-- progressive disclosure.
apps/chanora_flutter/lib/widgets/voice_compact.dart (new file)
* VoiceStatusChip: 2-line live readout. Line 1 = mode + bind hint;
Line 2 = release-tail + 'Mic on/off'. Tap opens
showVoiceDetailsSheet.
* VoicePttButton: 56 dp wide bottom-anchored Push to Talk button.
Same touch-and-hold gestures as the previous _PttHoldButton.
* showVoiceDetailsSheet: modal bottom sheet with mode recap +
bind/tail hint + level meter + TX/RX counts + PTT capability
badge (desktop-only).
apps/chanora_flutter/lib/main.dart
* AppBar gains mic-mute, headset-mute, voice-settings icons when
in voice channel AND MediaQuery width < 840 dp (mobile only).
Wide mode keeps these controls inside the existing VoiceBar
widget unchanged.
* AppBar title becomes a Row of 'app name + channel chip' when
in voice channel.
* Narrow-mode body restructured: Expanded(channelTree) +
VoiceStatusChip + VoicePttButton (latter only when in voice
channel AND PTT mode). The old narrow-mode VoiceBar is gone;
wide-mode VoiceBar is unchanged.
* New _onOpenVoiceDetailsSheet handler bridges the chip-tap to
showVoiceDetailsSheet.
* New top-level helper _isTouchOnlyPttHost mirrors the helpers
in widgets/voice_bar.dart and widgets/voice_settings.dart so
the AppBar + narrow-mode chip can branch consistently.
apps/chanora_flutter/lib/l10n/app_en.arb
apps/chanora_flutter/lib/l10n/app_zh.arb
apps/chanora_flutter/lib/l10n/generated/* (regenerated)
* New string voicePttHoldHint = 'Hold the button' / '按住按钮'.
Surfaced in line 1 of VoiceStatusChip on touch-only hosts and
in the modal sheet's PTT line where the desktop equivalent
would name a bound key.
Wide-mode (>= 840 dp) layout intentionally unchanged so the
signed-off rc.8 desktop verification still applies.
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
Local cargo + flutter analyze pass; Mac was offline at commit
time so iOS device build verification is pending the next sync.
|
||
|
|
7c0e5caa92 |
fix(ui,ios): first-tap on TextField now opens the keyboard
User report: 'every first time to tap to input box nothing
happened'. Symptom is iPhone-specific: the very first tap on any
of the Host / Nickname / Password text boxes in the connect form
fails to focus + open the keyboard. The second tap on the same
field works.
Two distinct causes, both addressed:
apps/chanora_flutter/lib/main.dart
The connect form lives inside a SingleChildScrollView. Flutter
on iOS has a long-standing issue (flutter#19027) where the
enclosing Scrollable's gesture-arena participant absorbs the
first tap as a possible scroll-intent, leaving the TextField
unfocused; the second tap reaches the field because the
scrollable has already declined to handle a drag.
Fix: _ConnectForm converted from StatelessWidget to
StatefulWidget so it can own FocusNodes for the three text
fields. Each TextField gains:
* focusNode: <its own FocusNode>
* onTap: () => focusNode.requestFocus()
— forces focus on tap-down regardless of arena outcome
* textInputAction: TextInputAction.next (host, nick) /
TextInputAction.done (password) for return-key flow
* autocorrect: false, enableSuggestions: false
— these are server-host / nickname / password fields, the
iOS auto-correct + suggestion bar is wrong for all three.
Also set keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior
.onDrag on the SingleChildScrollView so the keyboard hides
when the user starts scrolling the bookmark list below.
apps/chanora_flutter/ios/Runner/AppDelegate.swift
AVAudioSession.sharedInstance().requestRecordPermission was
fired synchronously from didFinishLaunchingWithOptions. The
permission alert can race with iOS's text-input subsystem
initialisation: if the alert appears before the keyboard
layer finishes wiring up, subsequent text-field focus
requests are dropped silently.
Fix: DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) to
defer the permission request until ~1 s after the app shell
is on screen. Long enough for iOS's text-input layer to fully
initialise; short enough that the user reads the prompt
before tapping a field.
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
flutter build ios --release --no-codesign: 26.8 s clean
(Runner.app 29.8 MB).
|
||
|
|
d4c04b6a72 |
fix(ui,audio,ios,macos): eight P0 mobile fixes
User report from sideloaded iPhone build, in order of priority: #4 'Could not join channel: audio: audio backend: build_output_stream: The requested stream configuration is not supported by the device.' Cause: we forced cpal::BufferSize::Fixed(2048) on the output and input streams unconditionally on non-Linux. iOS CoreAudio RemoteIO units reject arbitrary buffer-size requests with that exact error. Windows WASAPI needs the pinning for shared-mode jitter, but macOS / iOS do not. Fix: cfg-gate Fixed(2048) to target_os = 'windows'; everywhere else use BufferSize::Default and let the platform HAL pick. crates/chanora_audio/src/engine.rs. #5 'Could not join channel: invariant violated: voice_in already taken' Cause: start_audio tore down the old engine BEFORE attempting to construct the new one, and consumed voice_in (an mpsc Receiver that can only be taken once) early. When the new engine failed mid-construction (e.g. because of #4 above) the session was left with: no audio engine, voice_in consumed, no way to retry without reconnect. The second voice_join attempt surfaced the invariant message. Fix: build the new engine BEFORE tearing down the old. Only swap state.audio if construction succeeded. crates/chanora_ core/src/lib.rs::ChanoraSession::start_audio. Additionally added a put_voice_in helper to the protocol adapter ( crates/chanora_protocol/src/adapter.rs) for a future broadcast-channel migration; the helper is unused on the immediate fix path but documents the intent. #3 'permission request would better on first open' Cause: AVAudioSession only triggers the mic-permission prompt the first time it tries to record. We never recorded until voice_join, so the prompt fired then. Fix iOS: AVAudioSession.sharedInstance().requestRecordPermission in AppDelegate.swift::application(_:didFinishLaunchingWithOptions:). Fix macOS: AVCaptureDevice.requestAccess(for: .audio) in macos/Runner/AppDelegate.swift::applicationDidFinishLaunching. Both run non-blocking; user can deny without crashing app launch, and voice_join then surfaces a clearer downstream error when the engine fails to open the input device. #1 + #2 'one-column upper takes too much space; Push to Talk button at bottom would be better' Layout rework for narrow-mode (single column, mobile shape): - Flipped the stacking order in main.dart so Voice Bar moves to the BOTTOM of the body and the channel tree (Expanded) fills above. Wide-mode (Row, >= 840 dp) layout unchanged. - Inside the Voice Bar on touch-only hosts, moved the on-screen Push to Talk button to be the LAST element of the Voice Bar (was Row 3). Order now: pill + mutes, mode badge + settings, level meter, stats line, release-tail caption, PTT button. The button is closest to the user's thumb when the Voice Bar is pinned to the bottom of a narrow-layout screen. #6 'remove right top debug badge' debugShowCheckedModeBanner: false on the MaterialApp. Release builds never showed it anyway; this only affects local dev / debug builds. #7 'what does the refresh button use for? nothing happened' Removed. The snapshot updates via BridgeEvent::SnapshotChanged are pushed from the bridge — a manual rust.snapshot() call was redundant. Now only the Diagnostics + Disconnect actions remain in the AppBar trailing row when connected. #8 'Bind Key related function should not be added to a mobile platform' widgets/voice_settings.dart: bind-key OutlinedButton is now #cfg'd out when Platform.isIOS || Platform.isAndroid. The release-tail slider stays because it still applies to the on-screen PTT button. Capability badge in voice_bar.dart also hidden on mobile (it would always show L0Focused which is redundant with the visible on-screen button). Tests + analyze: chanora_audio 34/0/0 on macOS, workspace 78/0/1 on Linux; flutter analyze clean (6 pre-existing Radio.groupValue infos). flutter build ios --release --no-codesign: 28.8 s clean (Runner.app 29.9 MB). |
||
|
|
278df25fd7 |
feat(ui,ptt): on-screen touch-and-hold PTT button for iOS / iPadOS / Android
iOS / iPadOS / Android have no hardware keyboard for the user to
bind a PTT key on. Up to now the VoiceBar showed only a
'Push to talk: bound key —' hint that didn't lead anywhere usable.
Add a touch-and-hold on-screen PTT button rendered only on
touch-only platforms (Platform.isIOS || Platform.isAndroid; web
hosts and desktop continue to use the hardware-key path
unchanged).
apps/chanora_flutter/lib/widgets/voice_bar.dart:
* New module-private `_isTouchOnlyPttHost` predicate.
* VoiceBar gains an `onPttHeldChanged: ValueChanged<bool>`
constructor param. Desktop callers wire it but never invoke it
because the button is not rendered there.
* Row 3 (the PTT-only secondary content) now branches:
- on touch-only hosts -> renders the new `_PttHoldButton` plus
a small release-tail hint underneath
- on hardware-keyboard hosts -> renders the same bound-key +
release-tail one-liner as before, unchanged.
* New `_PttHoldButton` StatefulWidget. Uses a single
GestureDetector covering onTapDown / onTapUp / onTapCancel /
onPanDown / onPanEnd / onPanCancel so the held edges fire on
finger-down and the released edge fires when the user lifts
OR drags off OR another gesture in the arena wins. Visual
feedback mirrors the level-meter active flag.
apps/chanora_flutter/lib/main.dart:
* New `_onOnscreenPttHeldChanged(bool held)` method that calls
`rust.setPtt(active: held)`. The bridge's set_ptt routes the
edge through the same release-tail timer + transmit-mode
selector that desktop hardware keys use (SDD-096 / SAD-083),
so behaviour parity is preserved.
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
flutter build ios --release --no-codesign: clean (Runner.app 29.9 MB).
DEC-025: iPhone + iPad + Android in scope; this commit makes PTT
mode actually usable on those platforms. The 'Focused' capability
badge wording in ios-p0-acceptance.md / ipad-p0-acceptance.md
already documents the on-screen button as the only PTT input;
this commit makes that documentation true.
|
||
|
|
f5d3810f5f |
feat(ios,macos): Apple privacy manifest (PrivacyInfo.xcprivacy)
Apple has enforced a `PrivacyInfo.xcprivacy` privacy manifest at App
Store submission since May 2024 for iOS / iPadOS / visionOS /
watchOS, and rolled the requirement out to macOS in late 2024.
Without the file, App Store Connect rejects archive uploads with
"missing required privacy manifest". This commit adds the manifest
for both iOS and macOS Runner targets.
apps/chanora_flutter/ios/Runner/PrivacyInfo.xcprivacy
apps/chanora_flutter/macos/Runner/PrivacyInfo.xcprivacy
Identical content. Declarations:
NSPrivacyCollectedDataTypes:
NSPrivacyCollectedDataTypeAudioData
Microphone audio transmitted to the user's chosen voice
server while connected and unmuted. Not linked to user
identity (no Apple ID / IDFA tied), not used for tracking.
Purpose: AppFunctionality (communications).
NSPrivacyTracking: false
NSPrivacyTrackingDomains: []
Chanora performs no cross-app / cross-website tracking.
NSPrivacyAccessedAPITypes:
FileTimestamp (C617.1)
tokio + rusqlite file I/O for identity.tskey, chanora.db,
audio_meta.json, chanora.log inside the app container.
UserDefaults (CA92.1)
Indirect via path_provider Flutter plugin querying for
Application Support / Documents directories.
SystemBootTime (35F9.1)
tracing-subscriber timestamps log records relative to boot.
DiskSpace (85F4.1)
rusqlite checks before sqlite page writes.
All four "required reason" API categories use Apple's published
allow-list reason codes; no fingerprinting / analytics usage.
apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj
apps/chanora_flutter/macos/Runner.xcodeproj/project.pbxproj
Added PrivacyInfo.xcprivacy to the Runner group and to the
Runner target's "Copy Bundle Resources" build phase via the
xcodeproj Ruby gem (via a one-shot script). With this, the file
is placed at Runner.app/PrivacyInfo.xcprivacy where Apple's
validator looks for it — `find Runner.app -name
PrivacyInfo.xcprivacy` shows our manifest at the bundle root
alongside Flutter's and connectivity_plus's.
Verified on the M1 Mac (coder@100.118.130.73):
flutter build ios --release --no-codesign 4.0 s
-> Runner.app/PrivacyInfo.xcprivacy present
flutter build ipa --release --no-codesign 28.4 s
-> Runner.xcarchive built (171.4 MB)
-> archive's Runner.app/PrivacyInfo.xcprivacy present
-> archive's Runner.app/Frameworks/chanora_bridge.framework
built fresh via the chanora_bridge.podspec prepare_command
under xcodebuild's sandbox (no PATH / env weirdness).
P1 follow-ups noted by xcodebuild's validator (not blockers for
this commit but for App Store submission):
* Real app icon (currently default placeholder)
* Real launch image (currently default placeholder)
* Paid Apple Developer Program account, registered App ID, and
Distribution provisioning profile (Personal Team sideloads
still work as today).
|
||
|
|
1f729284b3 |
feat(flutter,ios): vendor chanora_bridge as a CocoaPods framework
iOS rejects loose .dylib loads (`dlopen` of any path outside the
app bundle is sandboxed), so a Flutter-Rust bridge has to ship
inside the .app as an Embed-and-Sign framework that
flutter_rust_bridge's runtime loader can dlopen via its default
`chanora_bridge.framework/chanora_bridge` lookup path.
This commit wires the bridge into the iOS build via a CocoaPods
podspec. Same role Cargokit plays for other Flutter+Rust setups,
done by hand against this repo's layout to avoid the Cargokit
vendoring footprint that was previously dropped.
apps/chanora_flutter/ios/chanora_bridge.podspec
New file. `prepare_command` invokes
`cargo build --release --target aarch64-apple-ios -p chanora_bridge`
with IPHONEOS_DEPLOYMENT_TARGET=13.0 +
CMAKE_POLICY_VERSION_MINIMUM=3.5 (satisfies audiopus_sys's
cmake invocation on modern CMake 4.x), then wraps the
produced libchanora_bridge.dylib into
chanora_bridge.framework with an Info.plist that declares
iPhoneOS / MinimumOSVersion=13.0, and rewrites LC_ID_DYLIB
to @rpath/chanora_bridge.framework/chanora_bridge.
`vendored_frameworks` exposes the result to CocoaPods, which
integrates it into Runner.xcodeproj with Embed & Sign
automatically. No Xcode UI edits required.
apps/chanora_flutter/ios/Podfile
Add `pod 'chanora_bridge', :path => '.'` to the Runner
target.
apps/chanora_flutter/ios/Podfile.lock
Generated by `pod install` after the pod was added. Pins
chanora_bridge 1.0.0 + checksum so iOS builds on other
developer machines pull the same framework version.
.gitignore
Add `/apps/chanora_flutter/ios/Frameworks/` so the
~13 MB built framework (regenerated on every pod install) is
not committed.
Verified end-to-end on the M1 Mac (coder@100.118.130.73):
pod install ok
chanora_bridge.framework generated at
apps/chanora_flutter/ios/Frameworks/chanora_bridge.framework
Install name: @rpath/chanora_bridge.framework/chanora_bridge
flutter build ios --release --no-codesign 12.7 s
-> Runner.app 29.9 MB (was 16.9 MB without the bridge)
-> Runner.app/Frameworks/chanora_bridge.framework present
alongside Flutter.framework, App.framework,
connectivity_plus.framework, objective_c.framework.
DEC-025: iOS / iPad officially in scope for P0.
Notes for owners installing on physical iOS devices:
The Personal Apple Team in Xcode requires a unique
PRODUCT_BUNDLE_IDENTIFIER (the default 'app.chanora.chanoraFlutter'
may already be claimed in the App Store registry). Set this
locally in Xcode -> Runner target -> Signing & Capabilities ->
Bundle Identifier (e.g. yourname.chanora.chanoraFlutter); do
NOT commit that change back since it is user-specific.
|
||
|
|
6d094f3dbe |
docs(verification,index): iPad p0 acceptance checklist + 0.9.9 index row
New controlled document docs/verification/ipad-p0-acceptance.md
extends iOS P0 coverage to iPad. The build artefact is identical
to iPhone — `TARGETED_DEVICE_FAMILY = "1,2"` in
ios/Runner.xcodeproj/project.pbxproj is the Universal family, so
the same Runner.app installs on iPad with the same personal-team
provisioning profile.
15-row checklist mirrors ios-p0-acceptance.md TC-1..TC-12 and adds
three iPad-specific rows:
TC-13 wide-mode landscape layout — iPad in landscape is well
above the 840 dp LayoutBuilder breakpoint shipped in
apps/chanora_flutter/lib/main.dart, so the connected view
splits into a 320 dp left column (banner + Voice Bar) plus
an expanding channel tree. Portrait rotation collapses
back to the stacked iPhone layout. Long channel-name pills
still ellipsize per the earlier voice_bar.dart fix.
TC-14 Split View / Slide Over no-crash — Apple iPad multitasking
is intentionally unsupported in P0. UIApplicationSupports\
MultipleScenes stays false. This row asserts the app does
not crash when iPadOS tries to host it in Split View; the
actual multi-scene wiring is P1.
TC-15 AirPlay 2 audio route — verifies AVAudioSession routing
honours an AirPlay 2 destination picked via Control
Center, and routes back cleanly when iPad is reselected.
docs/governance/document-index.md
Bumped to 0.9.9 with the change-history row noting the iPad
acceptance doc. No spec items added; DEC-025 was originally
iPhone-only for the mobile target and this row formally
extends P0 coverage to iPad within the same iOS toolchain.
python3 tools/validate_docs.py: clean (pre-existing 35-filename
[FAIL] retained, unchanged).
|
||
|
|
f0ddb160a0 |
fix(protocol,audio,ios): native-tls instead of rustls+aws-lc-rs
Building the bridge for `aarch64-apple-ios` failed in two ways with
the previous TLS stack:
1. `aws-lc-sys` (transitive: rustls -> aws-lc-rs -> aws-lc-sys)
does not cross-compile cleanly to iOS — the build produced
undefined symbols for architecture arm64 (mldsa44, ec_GFp_mont,
etc).
2. `audiopus_sys` linked against the wrong iOS runtime version,
missing `___chkstk_darwin`.
Following the rustls-platform-verifier docs and the standard Rust+
iOS+TLS pattern used by 1Password / Signal / rustup / Bitwarden,
this commit swaps the TLS provider to **native-tls** so each
platform picks its own:
* macOS + iOS -> Security.framework (no external C deps)
* Windows -> SChannel
* Linux/BSD -> system OpenSSL
Changes:
crates/chanora_protocol/Cargo.toml
crates/chanora_audio/Cargo.toml
* Drop `default-tls` from tsclientlib's features. The remaining
`audio` feature is what we actually use; default-tls was a
reqwest convenience that picked rustls+aws-lc-rs.
* Add a direct `reqwest` dep with `default-features = false,
features = ["charset", "http2", "native-tls"]`. Cargo's
workspace feature unification carries this through the
transitive `tsclientlib -> reqwest` chain.
apps/chanora_flutter/ios/Podfile
* Uncomment `platform :ios, '13.0'` so CocoaPods stops emitting
the implicit-platform warning and Xcode's iOS deployment-
target check is honored.
apps/chanora_flutter/ios/Podfile.lock
* Generated by `pod install` after the platform pin. Committed so
iOS builds on other developer machines pull the exact same Pod
versions.
apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj
apps/chanora_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata
* CocoaPods auto-integration: adds Pods_Runner.framework +
Pods_RunnerTests.framework references and the Pods xcconfig
file references. Standard `pod install` output; reviewing the
diff shows only Pod-bookkeeping additions, no signing or
target-config drift.
Verified end-to-end on the M1 Mac (coder@100.118.130.73):
cargo build --release -p chanora_bridge 29.09 s
cargo build --release --target aarch64-apple-ios 24.32 s
(with IPHONEOS_DEPLOYMENT_TARGET=13.0 and
CMAKE_POLICY_VERSION_MINIMUM=3.5 in the env to satisfy the
audiopus_sys cmake invocation; documented as a P1 build-glue
follow-up.)
flutter build ios --release --no-codesign ok
Built build/ios/iphoneos/Runner.app (16.9 MB)
Xcode GUI build of Runner.xcworkspace ok
(after the user opened Runner.xcworkspace, NOT
Runner.xcodeproj, and Clean Build Folder.)
Tests on macOS unchanged: chanora_audio 34 / 0 / 0.
DEC-025: iOS + macOS officially in scope for P0.
|
||
|
|
41d5a4b91b |
docs(verification,index): macOS + iOS p0 acceptance checklists + 0.9.8 index row
New controlled documents:
* docs/verification/macos-p0-acceptance.md
15-row human-must checklist for Apple Silicon macOS P0 sign-off.
Targets the SDD-085 CGEventTap backend (now fully live) +
SDD-094..097 audio lifecycle. Auto-test rows pre-filled from the
M1 Mac verification pass: chanora_audio 34 / 0 / 0 (Linux: 32;
macOS delta is the keymap + Send-bound + descriptor-builder
tests). Pre-flight covers the manual framework-wrap +
install_name_tool + ad-hoc codesign step via the new
tools/macos-postbuild.sh. TC-3 walks the Input Monitoring grant
+ descriptor watch transition timing (~1.5 s).
* docs/verification/ios-p0-acceptance.md
12-row human-must checklist for iOS P0 sign-off on a physical
iPhone via the developer's free Apple Personal Team. TC-3
documents the Focused-only PTT capability iOS gives us (no
global event tap analogue exists). TC-8 covers UIBackgroundModes
= audio. TC-9 covers AVAudioSession routing — phone-call
interruption, AirPods route, etc.
docs/governance/document-index.md
Bumped to 0.9.8 with a change-history row covering both new
acceptance documents. No spec items added; the existing SDD-085
(macOS) and SDD-094..097 (audio lifecycle) are what these
documents sign off.
python3 tools/validate_docs.py: clean (pre-existing 35-filename
[FAIL] retained, unchanged).
|
||
|
|
f3320715ea |
feat(audio,ios): AVAudioSession PlayAndRecord+voiceChat in AppDelegate
iOS AVAudioSession must be configured BEFORE Flutter starts its
audio pipeline; the canonical place is application(_:didFinishLaunching\
WithOptions:) in AppDelegate.swift. This commit:
apps/chanora_flutter/ios/Runner/AppDelegate.swift:
* import AVFoundation
* In application(_:didFinishLaunchingWithOptions:), call
AVAudioSession.sharedInstance().setCategory(.playAndRecord,
mode: .voiceChat,
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
followed by setActive(true). Failures are NSLogged but do not
block app launch — cpal's CoreAudio backend will still come up
against the default iOS routing.
This shape:
* routes the receiver/speaker like a phone call (.playAndRecord +
.voiceChat),
* engages on-device AEC / NS where supported,
* defaults to speaker so users don't have to hold the phone to
their ear,
* permits Bluetooth headsets (AirPods et al. just work).
crates/chanora_audio/src/engine.rs:
* Replace the iOS engine-start placeholder log line ('binding
pending — Chanora iOS audio is documented-only for Beta') with
an honest acknowledgment that the AVAudioSession configuration
lives Swift-side. The Rust engine acknowledges the request, then
cpal opens its CoreAudio streams against the session.
iOS-only Rust code is #[cfg(target_os = "ios")]-gated so this commit
is no-op on every other platform.
SRS-197: iOS/macOS audio routing contract. DEC-025: iOS officially
in scope for P0 (Focused PTT only — Apple's sandbox model has no
global PTT analogue).
|
||
|
|
383b707e7c |
feat(audio,macos): live CGEventTap PTT capture (SDD-085)
Replace the macOS PTT backend's worker-thread stub (which had just
slept) with a full CGEventTap implementation:
* extern "C" bindings to CGEventTapCreate, CGEventGetIntegerValueField,
CGEventTapEnable, CFMachPortCreateRunLoopSource, CFRunLoopGetCurrent,
CFRunLoopAddSource/RemoveSource, CFRunLoopRun/Stop, CFRelease, plus
the kCFRunLoopCommonModes static.
* tap_callback: C-ABI extern fn that reads bound keycode /
mouse-button from atomics, matches the incoming event, and toggles
the AudioTransmitGate. Returns the event unchanged (listen-only
tap, no event modification). Privacy-safe: never logs raw key
codes or button numbers (DEC-027).
* Event mask covers kCGEventKeyDown, kCGEventKeyUp,
kCGEventOtherMouseDown, kCGEventOtherMouseUp; also handles the
kCGEventTapDisabledBy{Timeout,UserInput} notifications by logging
a degraded-mode warning.
* Worker thread captures the CFRunLoopRef via a Send-marked
RunLoopHandle newtype so stop() can call CFRunLoopStop from the
audio engine thread.
* Box<TapState> is leaked into the worker via a Send-marked
TapStatePtr newtype; reclaimed on worker exit so the gate's Arc
refcount stays correct.
* Flutter logical-key labels are mapped to Carbon virtual keycodes
via label_to_macos_keycode (covers letters, digits, function keys,
navigation, common punctuation). Mouse-side-button labels resolve
via label_to_macos_mouse_button (3 = Mouse4, 4 = Mouse5).
* refresh_bound_atomics() rebuilds bound_keycode + bound_mouse_button
on start() and rebind() so the tap callback sees the new binding
without re-arming the tap.
Tests: chanora_audio 34 / 0 / 0 on macOS (was 28 before this commit).
Added: keymap_letters, keymap_function_keys, keymap_navigation,
keymap_unknown_returns_none, mouse_button_map, runloop_handle_is_send.
Verified the live IOHIDCheckAccess returns Undetermined (Unknown=2) on
a fresh M1 box where Input Monitoring has never been requested; the
1.5 s permission-watcher re-publishes the descriptor on user grant
or revoke without restart.
DEC-025: macOS desktop officially in scope. SAD-073: two-level PTT
ladder. SRS-198: honest capability advertising. DEC-027: privacy.
|
||
|
|
e3d7017dd9 |
feat(macos,ios): entitlements + permission strings + bundle-glue script
macOS:
* Runner/DebugProfile.entitlements + Release.entitlements: add
com.apple.security.network.client (outbound TS3 server connect)
and com.apple.security.device.audio-input (microphone capture).
Debug keeps com.apple.security.network.server + cs.allow-jit
(Flutter hot-reload needs both); Release drops them.
* Runner/Info.plist: add NSMicrophoneUsageDescription and
NSInputMonitoringUsageDescription so the macOS system prompts
show a sensible explanation when Chanora first needs mic or
Input Monitoring access. Input Monitoring is required by
CGEventTapCreate (SDD-085).
* Runner.xcodeproj/project.pbxproj: switch Debug/Release/Profile
code-signing from Automatic + Apple Development to Manual +
"Sign to Run Locally" (CODE_SIGN_IDENTITY = -). This lets
`flutter build macos --release` work over SSH where the login
keychain is locked. The owner re-enables the personal team
locally in Xcode for physical-device iOS testing later.
iOS:
* Runner/Info.plist: add NSMicrophoneUsageDescription and the
UIBackgroundModes = ['audio'] entry so voice traffic continues
when the app is backgrounded (TS3 servers drop clients on idle
audio streams).
tools/macos-postbuild.sh: new script. flutter build macos --release
emits build/macos/Build/Products/Release/chanora_flutter.app but
does NOT bundle libchanora_bridge.dylib. FRB on macOS dlopen()s the
bridge as chanora_bridge.framework/chanora_bridge, not a plain
dylib. This script:
1. Wraps target/release/libchanora_bridge.dylib in a proper
chanora_bridge.framework (Versions/A layout, Info.plist,
Resources, symlinks).
2. Rewrites LC_ID_DYLIB to
@rpath/chanora_bridge.framework/chanora_bridge.
3. Ad-hoc codesigns the framework and the .app bundle.
4. Verifies with codesign --verify --deep --strict.
macOS analogue of buildit.cmd on Windows. Auto-integration into
Xcode build phases via cargokit / corrosion is a P1 carryover.
Verified end-to-end on the M1 Mac:
cargo build --release -p chanora_bridge 11.76 s
flutter build macos --release ok (59.2 MB)
tools/macos-postbuild.sh Release ok
chanora_flutter.app launch via SSH bridge initialised,
identity + bookmark
store initialised
(~5 s smoke).
~/Library/Logs/app.chanora.chanora_flutter/chanora.log captures
the boot sequence cleanly.
DEC-025 reference: macOS desktop is officially in scope.
|
||
|
|
ade50488d9 |
feat(audio,macos): live IOHIDCheckAccess for Input Monitoring permission
Replaces the macOS PTT backend's query_permission() stub (which had
returned Undetermined unconditionally) with a real IOKit call:
extern "C" { fn IOHIDCheckAccess(request_type: u32) -> u32; }
IOHIDCheckAccess(kIOHIDRequestTypeListenEvent = 1)
Returns Granted (0), Denied (1), or Unknown (2). The existing 1.5 s
re-query worker now drives real descriptor transitions when the user
grants or revokes Input Monitoring in System Settings: the watch
sender republishes the descriptor, ChanoraSession forwards
BridgeEvent::PttCapability, and the Flutter capability badge updates
within ~1.5 s without an app restart.
Verified live on the M1 Mac:
rustc /tmp/check_perm.rs && ./check_perm
IOHIDCheckAccess(ListenEvent) = 2 (Unknown)
This is the expected initial state on a fresh box where Chanora has
not yet attempted CGEventTapCreate; once the next commit lands the
event-tap worker, the macOS Input Monitoring prompt will fire on
first audio start and the value transitions to Granted/Denied.
Tests: chanora_audio 28 / 0 / 0 on macOS (Linux had 32; the 4-test
delta is the Linux-only portal probe tests). The existing 7 macOS
backend unit tests still cover the descriptor builder + state
machine purely; they don't exercise the live IOKit call (which
would need a TCC-aware test harness).
SDD-085 reference: macOS Event Tap backend / L2 / L3 capability;
SRS-198 honest capability advertising.
|
||
|
|
4d57d189c9 |
feat(flutter,macos,ios): scaffold platform Xcode projects via flutter create
Ran `flutter create --platforms=macos,ios --project-name=chanora_flutter
--org=app.chanora .` on the M1 Mac to generate the standard Flutter
platform-specific scaffolding (Runner.xcodeproj, Podfile, AppDelegate,
entitlements, etc.) for both macOS and iOS.
The cross-platform Dart source (lib/) and Rust workspace (crates/,
core/) carry the actual application logic; these scaffolds are
required only so flutter build macos / ios can resolve their Xcode
projects. No application code added.
macOS smoke-launch from the M1 Mac verified the bridge dylib load
path: after manually wrapping libchanora_bridge.dylib into a proper
chanora_bridge.framework bundle (FRB on macOS expects a framework,
not a plain dylib) and codesigning ad-hoc, the runner starts cleanly
through 'bridge initialised', 'identity store initialised', 'bookmark
store initialised' just like the Linux runner.
Following commits will:
* Wire the framework-bundling step into build glue (currently manual
install_name_tool + codesign).
* Replace the macOS PTT backend stub (crates/chanora_audio/src/
ptt_backends/macos.rs) with live IOHIDCheckAccess +
CGEventTapCreate so the descriptor advertises real L2/L3 capability
on a permission-granted box (SDD-085).
* Add the iOS AVAudioSession PlayAndRecord+voiceChat wiring.
* Add docs/verification/macos-p0-acceptance.md and ios-p0-acceptance.md.
|
||
|
|
cd402f164a |
docs(verification,index): linux p0 acceptance checklist + 0.9.7 index row
New controlled document docs/verification/linux-p0-acceptance.md mirrors
docs/verification/windows-p0-acceptance.md with 15 TC rows tuned for the
GNOME-on-Wayland target environment (DEC-025). Pre-flight calibrated to
the Arch verification host (100.74.219.114): pacman queries, path
prefixes, xdg-desktop-portal-gnome version notes. Auto-test sign-off
filled with the headless verification pass run over SSH:
cargo check --workspace --release clean (30.99 s)
cargo test --workspace --lib 78 / 0 / 1
cargo test ... linux_portal_smoke -- --ignored
-> 1 / 0 (GlobalShortcuts portal reachable, version = 1)
cargo test ... ptt_privacy 1 / 0 (DEC-027 holds)
The 15 GUI rows are marked pending physical-console pass. SDD-086 portal
flow is what this doc signs off; SDD-081/094..097 are referenced.
Document index bumped to 0.9.7 with the change-history row covering the
new acceptance doc. No spec items added; pre-existing 35-filename FAIL
in validate_docs.py retained.
|
||
|
|
25b5bb3a6d |
fix(ui): wide-mode banner placement + channel-pill overflow
Two related VoiceBar / scaffold issues on wide windows: 1. Banner placement The 'not production ready' tertiaryContainer banner sat full-width above the body Column. In wide layouts (>=840 dp) where the connected view splits into Voice Bar (320 dp) + channel tree (Expanded), the banner spanned both columns and dwarfed the channel-tree pane. Rework: wrap the body in an outer LayoutBuilder so the placement decision can read bodyConstraints.maxWidth. When wide AND connected AND snapshot != null, render the banner inside the left 320 dp SizedBox above the VoiceBar. In every other state (narrow, idle, connecting) the banner stays pinned full-width at the top. 2. Channel-name pill overflow The Container holding the channel pill had no width constraint and Text(channelName) had no overflow handling. Long channel names made the pill extend past the column's 320 dp; mute icons slid under the adjacent channel tree. Rework: pill wrapped in Flexible(flex: 100, fit: FlexFit.loose); inner Text gets maxLines: 1, overflow: TextOverflow.ellipsis, softWrap: false. Spacer keeps default flex 1; the 100:1 ratio means short names hug their intrinsic width and long names take ~99% of the remaining space then ellipsize. Mute icons stay pinned right. flutter analyze: clean (6 pre-existing Radio.groupValue infos only). |
||
|
|
d9c330c23b |
feat(audio,linux): output via SDL2; cpal stays on Windows/macOS
User reported persistent crackling/popping from peer audio on Linux even
after fixing the 48k->device-rate resampler boundary discontinuities,
clamping pre-Opus-encode peaks, and pre-allocating the playback scratch
buffer. Logs confirmed cpal opened raw ALSA at 44.1k native, no callback
budget violations, no underrun warnings -- yet the audio was still poor.
Root cause: cpal on Linux opens raw ALSA's 'default' PCM. On modern
PipeWire / pipewire-alsa boxes that virtual device routes through ALSA's
dmix + plug layers, whose default resampler is nearest-neighbour. cpal
also picks a small default period size (~256 frames / 5.8 ms) leaving no
headroom for kernel scheduler jitter. Both effects compound into the
crackling the user heard.
Upstream tsclientlib's own audio example
(tsclientlib/examples/audio_utils/ts_to_audio.rs) and the official Qint
client both use SDL2 with AudioSpecDesired { freq: 48000, channels: 2,
samples: 960 }. SDL2 on the same systems routes through PipeWire's PA
bridge (or PulseAudio directly), both carrying high-quality resamplers.
Fix:
* Add sdl2 = '0.37' as a target_os=linux dependency. Links libSDL2-2.0
.so (Arch sdl2-compat over SDL3, Debian libsdl2-2.0-0, Fedora SDL2).
* New module crates/chanora_audio/src/sdl_output.rs implementing
SdlOutput: opens a 48 kHz stereo 960-frame callback that zeroes the
buffer and calls AudioHandler::fill_buffer directly (no user-side
resampler). Master gain + hard-mute atomics wired in identically to
the cpal callback so set_output_gain / set_output_muted keep working.
* engine.rs cfg-gated: target_os='linux' builds SdlOutput; everywhere
else continues with the cpal output path (including the device-native-
rate negotiation and resampler-continuity fixes shipped earlier --
those remain correct on Windows/macOS where cpal targets WASAPI /
CoreAudio cleanly).
* The cpal output helpers (build_output_stream, PlaybackResampleState,
FromF32) are now cfg(not(target_os='linux'))-gated so the Linux
build doesn't emit dead-code warnings.
Capture path still cpal on every platform -- outbound audio was not
reported as bad. Resampler-continuity fix on the capture side stays:
microphone -> Opus encoder still goes through the linear interpolator
with the last-sample anchor.
Tests: 32 / 0 / 0 (chanora_audio), workspace 78 / 0 / 1 unchanged.
|
||
|
|
8acd456af1 |
feat(protocol): per-platform TS3 client_version selection
ConnectOptions previously took tsclientlib's default Version. Servers that strictly check the announced client signature could refuse or downgrade those sessions. Add pick_client_version() that selects a stable signed descriptor matching the runtime OS: Windows -> Version::Windows_5_0_0_beta51 Linux -> Version::Linux_5_0_0_beta51 macOS -> Version::macOS_5_0_0_beta51 Android -> Version::Android_3_5_0__7 iOS -> Version::iOS_3_5_6 other -> Linux fallback All five variants are guaranteed to exist in the vendored tsproto-types enum at compile time; build fails loudly if upstream removes one. Wired into Connection::build(...).version(pick_client_version()) on every connect. Emits 'selected TS3 client_version' info log line so the choice is visible in chanora.log. |
||
|
|
73066749e3 |
fix(audio,linux): isolate zbus blocking probe on a fresh OS thread
LinuxGnomeWaylandBackend::probe() called zbus::blocking::Connection::session() directly. The blocking facade internally constructs a current-thread tokio runtime and block_on()s its async D-Bus client. probe() runs from PttController::new (sync) which is called from start_audio (async on the bridge tokio runtime). Nested runtimes panic with 'Cannot start a runtime from within a runtime'. Symptom on Linux: the first voice-channel join surfaced a SnackBar 'Could not join channel: join: task N panicked ...' while the channel-move command had already succeeded server-side. User saw 'channel joined but voice not enabled'. Fix: run the cheap blocking probe on a dedicated std::thread (no ambient runtime), join it synchronously, propagate the version / error. Probe is microseconds; the join cost is negligible. |
||
|
|
6df5ea960e | chore: ignore opencode.json agent-local config | ||
|
|
61798e5bdf |
docs(verification,index): update test counts + add 0.9.6 row for rc.8 acceptance
- windows-p0-acceptance.md: bump auto-test row to reflect current Windows test count (126/0/1 — was 124/0/2 before the two new start_flips_armed_to_l2 tests landed). - document-index.md: new 0.9.6 row marking DEC-031 (missed-key-up watchdog disabled on P0) and the controlled status of docs/verification/windows-p0-acceptance.md. |
||
|
|
01003b3448 |
refactor(protocol): clarify pending_moves expiry sweep
The original mem::replace + retain pattern worked but was opaque. Switch to a two-pass approach: collect expired MessageHandles into a small Vec, then remove + resolve. Behaviour-preserving; the clippy-style readability win is worth the tiny extra allocation (typical case: 0 or 1 expired entries per loop iteration). |
||
|
|
2511b24982 |
fix(audio,protocol,ui): TC-2.3 + TC-10 + TC-13 + channel tree hierarchy
Five user-reported defects + one auto-test regression-catcher.
== TC-2.3: capability badge stuck at L0Focused on Korean Win 11 ==
Root cause: in WindowsRawInputBackend::start() (and the parallel
WindowsHookBackend), the worker thread's armed.store(ok, ...) only
ran AFTER GetMessageW returned (i.e. on WM_QUIT). During normal
arming the message pump runs forever, so armed stayed at its
initial false value, and descriptor() reported L0Focused even
though RegisterRawInputDevices had succeeded.
Fix: run_raw_input_loop and run_hook_loop now take armed as a
parameter and flip it to true inside the loop right after the
successful registration, before blocking on GetMessageW. The
outer setter is kept as a belt-and-braces clear-on-failure path.
Also drops the redundant outer 'Raw Input armed' / 'low-level
hook armed' log lines — the in-loop 'Raw Input devices
registered' / 'low-level hooks installed' messages already
convey arming success with full context.
New tests raw_input_backend_start_flips_armed_to_l2 and
hook_backend_start_flips_armed_to_l2 call real start(), sleep
80 ms, assert descriptor().level == L2GlobalHoldToTalk. Replaces
the previous #[ignore]'d real-start smoke test which never
asserted on the descriptor.
== TC-10: no-permission channel rejection invisible ==
Root cause 1: chanora_protocol::adapter::move_self_to used the
fire-and-forget send() on the client_move command. TS3 server
replies with a typed error event the adapter discarded, and
move_to_channel returned Ok regardless.
Root cause 2: even when chanora_core::voice_join detected the
non-confirmation via snapshot polling, the rolled-back error
flowed into the connect-form-area _error string which is hidden
post-connect. The user saw no feedback.
Fix:
* New ProtocolError::ServerRejected { code: u32, message: String }
carries the canonical TS3 error code per the official catalogue
at https://github.com/ReSpeak/tsdeclarations (Errors.csv).
* move_self_to now uses send_with_result, returns a MessageHandle.
The connection-task loop holds a pending_moves HashMap keyed by
MessageHandle, services StreamItem::MessageResult by looking up
and resolving the reply with either Ok or the typed
ServerRejected.
* Pending entries have a 3 s deadline so a server that never
replies doesn't leak the reply channel — expired entries fall
back to Ok and let the snapshot poll handle confirmation.
* voice_join short-circuits on ServerRejected (no need for the
full snapshot poll), still polls for confirmation as a
belt-and-braces fallback for legacy servers; on poll failure
emits ServerRejected with sentinel code 0x0001 (undefined).
* New BridgeError::ServerRejected mirror with the same fields;
CoreError → BridgeError mapping preserves the typed variant.
* Flutter _onJoinChannel shows a floating SnackBar with a
localised message selected by error code (channelJoinFailed*
l10n entries). 6 known codes mapped to specific messages
(insufficient permission, wrong password, channel full,
family limit, private channel, timeout); everything else
falls back to the server-supplied generic message.
== TC-13: mouse side-button capture only works on text field ==
The _PttBindingCaptureDialog wrapped its Column with a Listener
using the default HitTestBehavior.deferToChild. Pointer events
landing on the dialog's empty padding regions weren't claimed by
any child and so were never delivered to the Listener.
Fix: explicit HitTestBehavior.opaque so the entire dialog area
catches PointerDown events regardless of where the cursor sits.
== Channel tree hierarchy ==
Reported issue: tree rendered as flat list, no indication of
parent-child nesting. The bridge already carries the
field; the renderer just ignored it.
Fix in _SnapshotView: walk the (already DFS-sorted) channel list
and compute each row's depth from its parent's depth. Render
left-padding of depth * 18 dp. Cap depth at 6 to keep deep
hierarchies visually bounded; the cap plateaus silently (no
glyph, channel still tappable, data carries the real depth).
== Responsive layout ==
Connected layout is now LayoutBuilder-driven. Below 840 dp wide
(Material's tablet/desktop breakpoint) the original stacked
column layout is used (Voice Bar on top, channel tree below).
At 840 dp and above the layout becomes a side-by-side Row with
the Voice Bar pinned at 320 dp on the left and the channel tree
Expanded on the right.
Verification
- cargo check --workspace: clean.
- cargo test --workspace --lib: 80 / 0 / 1 (unchanged Linux total;
+2 new Windows-only tests not counted here).
- flutter analyze: clean (6 pre-existing Radio.groupValue infos).
- FRB bindings regenerated to expose BridgeError_ServerRejected.
|
||
|
|
0b6ea11077 |
docs(verification): windows-p0-acceptance.md — human-side rc.8 sign-off
Captures the 15 TC rows the lead walks through on the Korean Win 11 host before tagging v1.0.0-rc.8, plus the auto-test sign-off matrix (Linux + Windows cargo + flutter analyze + validator + windows-smoke). Each TC row maps to a spec requirement (DEC / SRS / SDD) or to a regression the rc.7 review found. Failures block the tag. Known gaps recorded at the bottom: macOS / iOS / Android P0 are separate documents; VAD (DEC-030) and missed-key-up watchdog redesign (DEC-031) are P1; real RMS level meter is P1. |
||
|
|
881321595e |
test(ptt): use capital 'Space' in set_binding_updates_descriptor_watch
The Windows Raw Input backend's keymap is case-sensitive; the test passed lowercase 'space' which works for the Focused fallback on Linux but fails on Windows where resolve_binding returns InvalidBinding. Use the same canonical 'Space' string the bind dialog produces in real use. |
||
|
|
606d594dc9 |
fix(audio,windows): init_tx is SyncSender not Sender (windows-only build fix)
run_raw_input_loop / run_hook_loop signatures used Sender<bool> but the callers create the channel via sync_channel which returns SyncSender. Linux cross-check missed this because windows.rs is behind #[cfg(target_os = "windows")]. |
||
|
|
181b3368d4 |
fix(audio,voice,log): six P0 issues from Korean Windows test
1. Voice Bar 'Leave voice' button removed entirely. TeamSpeak users are always in some channel; Discord/Mumble-style leave is the wrong model. To stop being heard / hearing, mute mic / speaker. To physically move, tap a different channel. The voiceLeave bridge call + _onLeaveVoice stay as dead code for now (marked unused) so existing tests/integrations don't break. 2. voice_join now confirms the move actually applied server-side by polling the snapshot for up to 1.5 s and matching our own client's channel against the requested one. If the server rejected the move (no permission, wrong password, channel full), voice_join rolls the selector back to in_channel=false and returns Err so the UI surfaces the failure instead of showing a fake 'joined' state. 3. ServerSnapshot + BridgeSnapshot gain own_client_id so the UI can identify our row without name-matching. find_own_in reads it directly. 4. set_self_muted now also clamps the TransmitModeSelector's hard_mute when input is muted server-side. Without this, the Opus encoder kept producing frames after setInputMuted(true), tsclientlib refused each one with 'Sending audio while muted', and the log grew to 200 MB on the Korean host. 5. tsclientlib WARN spam suppressed via tracing filter (tsclientlib=error). Belt-and-braces on top of fix 4. 6. Log file is now rotated at every launch (not just when >4 MiB). Two generations kept: chanora.log.1 (previous) and chanora.log.2 (the one before). The bug that produced 200 MB files was a chatty subsystem flooding a single session; the per-launch rotate keeps disk use bounded by what one session can produce in its lifetime. Bonus Windows fix (separate from the six but found in the same log): the Raw Input + Hook backends now signal readiness BEFORE blocking on GetMessageW. Previously init_tx.send was called after the loop returned (i.e. on WM_QUIT, which never happens during arming), so the main thread's 2 s readiness probe always timed out and the backend reported L0Focused even when registration succeeded. Both run_raw_input_loop and run_hook_loop now take an init_tx parameter and call report!(true) right after a successful registration, and report!(false) on every early-fail return. cargo check --workspace: clean. cargo test --workspace --lib: 80 passed / 0 failed / 1 ignored. flutter analyze: clean (6 pre-existing Radio.groupValue infos). |
||
|
|
bb3c82e2d5 |
chore(version): bump to v1.0.0-rc.8 (post-rc.7 v1 audio + PTT lifecycle work)
pubspec.yaml + _kAppVersion in main.dart were both still reading v1.0.0-rc.1 even though the branch has accumulated 50+ commits of post-rc.7 work (the v1 audio + PTT lifecycle redesign in SDD-094..097, plus DEC-029/030/031). The About dialog and the diagnostic export's app_version field both surfaced the wrong string. Aligned both to v1.0.0-rc.8+59 (build number is the current commit count on the branch). The Rust workspace version stays at 0.0.1-pre — it's an internal pre-release marker the diagnostic export carries as crate_version, not user-facing, and changing it cascades into every inheriting Cargo.toml for no benefit. The actual v1.0.0-rc.8 tag will land once the Korean Windows 11 human-side P0 acceptance pass closes; this commit aligns the strings the running build shows so the tester sees a consistent version while running the tests. |
||
|
|
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.
|