chore: restore product scaffold to rollback baseline

This commit is contained in:
Edison Jwa
2026-05-29 14:02:04 +09:00
parent 2896f14ec9
commit fe6e07353e
434 changed files with 27278 additions and 63230 deletions
@@ -0,0 +1,116 @@
# Chanora Server Prefetch Crate Design
Date: 2026-05-28
## Goal
Move server-resolution prefetch policy out of `chanora_core` into a focused Rust crate named `chanora_server_prefetch`, without changing connection behavior, Flutter APIs, or protocol dialing semantics.
## Non-Goals
- Do not change resolver behavior or DNS/SRV/TSDNS ordering.
- Do not change `ConnectConfig.resolved_address` semantics in `chanora_protocol`.
- Do not expose prefetch state in the UI.
- Do not prefetch bookmarks or additional hosts.
- Do not persist prefetched addresses.
## Architecture
Add a workspace member at `crates/chanora_server_prefetch`.
Responsibilities:
- Normalize server host keys by trimming and lowercasing.
- Track one active prefetch generation.
- Store at most one successful prefetched socket address.
- Reject stale async completions by generation.
- Return a prefetched address only for an exact normalized host match.
- Enforce the 2-minute freshness TTL.
- Resolve server addresses by calling `chanora_resolver::ChanoraResolver::resolve_client_address`.
- Log prefetch start, success, miss/stale, and failure diagnostics.
Dependencies:
- `chanora_resolver` for actual server address resolution.
- `tokio` for `Mutex` and spawned prefetch tasks.
- `tracing` for diagnostics.
- `thiserror` for a narrow `ServerPrefetchError` public error type.
## Public API
The crate exposes this small async owner type:
```rust
pub struct ServerPrefetcher { ... }
impl ServerPrefetcher {
pub fn new() -> Self;
pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError>;
pub async fn fresh_match(&self, host: &str) -> Option<std::net::SocketAddr>;
}
```
`prefetch` returns after scheduling work, preserving the current invisible, non-blocking behavior. Empty normalized hosts are ignored successfully. Resolution failures are stored only as diagnostics and do not affect connect semantics.
## Core Integration
`chanora_core` replaces its private prefetch cache fields and helpers with `ServerPrefetcher`.
Core remains the trust boundary for connection config:
- It clears any caller-provided `ConnectConfig.resolved_address` before lookup.
- It asks `ServerPrefetcher::fresh_match` for the current host.
- It sets `dial_cfg.resolved_address` only from a fresh exact cache hit.
- It stores supervisor/reconnect config with `resolved_address: None`.
The Flutter bridge keeps calling the same core API, `prefetch_server_resolution(host)`. No Dart API change is intended.
## Data Flow
1. Flutter host editing schedules `ChanoraSession::prefetch_server_resolution(host)`.
2. Core delegates to `ServerPrefetcher::prefetch(host)`.
3. The prefetcher normalizes the host, increments generation, and spawns resolver work.
4. On success, the prefetcher stores the resolved socket address if the generation is still current.
5. On connect, core prepares `(stored_cfg, dial_cfg)`.
6. Core clears untrusted `resolved_address`, asks the prefetcher for a fresh exact match, and applies the result only to `dial_cfg`.
7. Protocol uses `dial_cfg.resolved_address` if present; otherwise it resolves normally.
## Error Handling
- Prefetch failures remain invisible to users.
- Prefetch failures are logged through `tracing`.
- If prefetch misses, is stale, or fails, connect falls back to normal protocol resolution.
- A caller-supplied `resolved_address` is never trusted by core.
## Testing
Move cache-policy tests from `chanora_core` into `chanora_server_prefetch`:
- fresh exact match returns the socket address.
- stale entries are ignored.
- different hosts are ignored.
- stale generation completions are ignored.
- blank normalized hosts return `Ok(())` and do not update generation or spawn resolver work.
Keep core tests for connection trust-boundary behavior:
- untrusted `resolved_address` is cleared on cache miss.
- prepared stored/supervisor config has `resolved_address: None`.
- prepared dial config can receive a fresh prefetched address.
Run at minimum:
- `cargo test -p chanora_server_prefetch --lib`
- `cargo test -p chanora_core --lib`
- `cargo test -p chanora_protocol --lib`
For final confidence, rerun the Android server connect smoke path that verifies prefetch logs and connected UI.
## Acceptance Criteria
- Workspace builds with the new crate member.
- `chanora_core` no longer owns the prefetch cache implementation.
- `chanora_server_prefetch` owns prefetch normalization, TTL, generation, storage, and resolver-backed warming.
- Public Flutter and Rust protocol behavior is unchanged.
- Existing server connect and reconnect safety tests pass.
- Android connect still reaches the connected server UI and does not get stuck in `Connecting` or `Synchronizing`.
@@ -0,0 +1,203 @@
# Server Resolution Prefetch Design
Date: 2026-05-28
## Purpose
Reduce perceived server join latency by resolving the active TeamSpeak server address before the user taps Connect. Prefetch must be invisible, conservative, and safe: it may warm resolver state, but it must not change connection semantics or surface background errors to the user.
Recent Android testing showed resolver latency can dominate the first part of the connect flow. A prior fix bounded slow TS3 SRV discovery and removed Android's forced Cloudflare resolver. Prefetch builds on that by hiding remaining address-resolution work when the user has already entered or loaded a likely server address.
## Goals
- Prefetch only the active server address field.
- Keep the feature invisible to users.
- Reuse prefetched results only for exact normalized host matches.
- Keep prefetched results fresh for 2 minutes.
- Preserve today's Connect behavior when prefetch misses, fails, or is stale.
- Avoid prefetching all bookmarks.
- Avoid opening a TS3 session before the user taps Connect.
## Non-Goals
- No visible resolving, ready, or failed UI state.
- No bookmark fan-out prefetch.
- No persisted resolver cache across app launches.
- No password, channel, or permission validation during prefetch.
- No server reachability probe beyond address resolution.
- No connection warm-up or pre-authentication.
## Chosen Approach
Use a Rust-owned resolver prefetch cache with Flutter-owned scheduling.
Flutter knows when the active host field changes, so it schedules prefetch requests. Rust owns resolver correctness, normalization, cache validity, and connect-time reuse. This keeps Flutter from depending on resolver internals and ensures Connect can independently decide whether a prefetched result is safe to use.
Other approaches considered:
- Flutter-only prefetch: rejected because it pushes resolver state into Dart and creates a weaker boundary between UI and connection behavior.
- Resolver-internal repeated-call cache only: rejected because it does not hide first-click latency from the active host field.
## Behavior
Prefetch starts for the active host value in two cases:
- After `_loadUiSettings()` loads the last-used host into `_hostCtl`.
- After the user stops editing the host field for about 700 ms.
The feature is invisible:
- No SnackBars.
- No inline status text.
- No disabled Connect button.
- No user-facing error if prefetch fails.
Connect behavior:
- If the current normalized host exactly matches a fresh prefetched entry, Connect uses the cached resolved address.
- If the cache is missing, stale, failed, or for a different host, Connect resolves normally.
- Connect remains the only operation that opens a TS3 session.
## Architecture
### Flutter Scheduling
`_BetaHomeState` owns the host text field. It should add a listener to `_hostCtl` and manage a short debounce timer.
Responsibilities:
- Trim the host input before scheduling.
- Skip empty values.
- Reset the debounce timer on each edit.
- Call a bridge prefetch API after about 700 ms of idle typing.
- Schedule one prefetch after settings load if the loaded host is non-empty.
- Dispose the listener and timer with the widget state.
Flutter does not store resolved addresses and does not decide whether Connect can use a prefetched result.
### Bridge API
Add a fire-and-forget bridge function shaped like:
```text
prefetch_server_resolution(host: String) -> Result<(), BridgeError>
```
The bridge call should return after the prefetch task has been accepted by the Rust runtime. It must not wait for resolution to complete. Background completion or failure is reported only through diagnostics/logging.
### Rust Resolver Cache
Rust stores a small prefetch cache owned near the session/resolver boundary. A single latest-host entry is enough for v1, because the design only prefetches the active field.
Cache entry fields:
- Normalized input host.
- Resolved `host:port` address.
- Resolution method.
- Completion timestamp.
- Generation or request id.
- Optional sanitized failure metadata for diagnostics.
The cache TTL is 2 minutes.
### Connect Integration
Connect should ask Rust for a fresh exact-match prefetched result before running normal resolution.
Rules:
- Exact normalized host match is required.
- Entry age must be at most 2 minutes.
- Failed entries must not block normal connect resolution.
- Stale entries must be ignored.
- Missing cache must behave exactly like today.
## Data Flow
1. App starts.
2. `_loadUiSettings()` loads the last-used host into `_hostCtl`.
3. Flutter schedules invisible prefetch for that host.
4. User edits the host field.
5. Flutter cancels the pending debounce timer and starts a new one.
6. After 700 ms idle, Flutter calls Rust prefetch with the latest trimmed host.
7. Rust normalizes and resolves the host through the same resolver path used by Connect.
8. Rust stores the result if it still matches the latest generation for that normalized host.
9. User taps Connect.
10. Rust Connect checks the cache for a fresh exact-match result.
11. Cache hit: Connect uses the prefetched address.
12. Cache miss/stale/failure: Connect resolves normally.
## Cancellation And Staleness
Cancellation can be logical rather than hard task cancellation.
- Flutter prevents obsolete debounce timers from firing.
- Rust tags requests by normalized host and generation.
- Late completions for stale generations must not replace newer successful entries.
- Duplicate prefetches for the same normalized host may coalesce or refresh the same entry.
This avoids complexity while preventing old input values from poisoning the cache.
## Error Handling
Prefetch failures are diagnostic-only.
- Empty host: skip prefetch.
- Invalid host shape: skip or fail silently with debug diagnostics.
- Resolver failure: store optional sanitized failure metadata for diagnostics only.
- Connect after failure: normal connect path runs and surfaces errors as it does today.
- App resume and network changes: no special invalidation in v1; TTL handles staleness.
- Disconnect: cache may remain because it is independent of the TS3 session.
## Diagnostics
Add privacy-safe logs for:
- Prefetch started.
- Prefetch result.
- Prefetch failed.
- Connect using prefetched resolution.
- Connect prefetch miss or stale entry.
Do not log passwords, channel passwords, or nickname. Host and resolved address are acceptable because resolver/connect logging already includes them today.
## Testing
Rust tests:
- Fresh exact-match prefetched result is reusable.
- Stale prefetched result is ignored.
- Different normalized host is ignored.
- Failed prefetch does not block normal resolution.
- Late stale generation cannot overwrite a newer cache entry.
Flutter tests should cover the scheduling logic through a small testable helper if wiring directly through `_BetaHomeState` would be brittle:
- Host edits debounce prefetch scheduling.
- Empty host does not prefetch.
- Settings-loaded host schedules one prefetch.
Manual Android smoke test:
- Install debug APK.
- Launch app.
- Wait for last-used host prefetch or type host and wait past debounce.
- Tap Connect.
- Confirm UI reaches connected server view.
- Confirm logcat shows either a prefetch cache hit or safe fallback behavior.
## Acceptance Criteria
- Typing or loading a valid host can warm resolver state before Connect.
- Connect never fails because prefetch failed.
- Connect never uses a prefetched result for a different normalized host.
- Prefetched entries older than 2 minutes are ignored.
- No visible UI is added for prefetch state.
- Bookmarks are not prefetched in bulk.
- Android debug build and focused resolver/Flutter tests pass.
## Implementation Notes
- Prefer a single latest-host cache unless implementation reveals an existing cache abstraction that makes a tiny map simpler.
- Prefer minimal bridge API surface: one prefetch call and connect-time internal cache lookup.
- Keep the resolver cache near existing Rust session/connect code so future non-Flutter clients can benefit from the same behavior.
@@ -0,0 +1,70 @@
# State Sync and UI Settings Validation Design
**Date:** 2026-05-29
**Status:** Approved for implementation
**Scope:** P0/P1 validation-based completion for state-sync evidence and UI settings persistence
## 1. Goal
Close the current DV/P0-P1 gaps for reducer/state-sync evidence and UI settings persistence with tests first, minimal behavior changes, and updated documentation evidence.
## 2. State-Sync Design
`chanora_state` remains the reducer owner. The validation pass adds focused tests for known reducer contracts rather than broad refactoring. Missing behavior is implemented only when a test proves a gap.
Required evidence covers:
| Contract | Evidence |
|---|---|
| Snapshot creates ready state and deterministic normalized order | Existing and expanded reducer tests |
| Reconnect discards stale state and reconnect snapshot replaces state | Existing reducer tests |
| Disconnected/lost states suppress live deltas | Existing reducer tests |
| Duplicate IDs are normalized deterministically | Existing reducer tests |
| Unknown client voice activity is ignored | Existing reducer tests |
| Channel deletion removes clients in deleted channel | New reducer regression test and implementation |
| Same event sequence produces same state and deltas | Existing reducer determinism test |
## 3. UI Settings Design
`UiPreferencesService` remains a Flutter service backed by `shared_preferences`. This is the minimal P0/P1-complete implementation because the current app already uses SharedPreferences and no current behavior requires SQLite-backed UI settings.
`UiSettings` gains a typed `themeMode` field with values:
| Value | Meaning |
|---|---|
| `system` | Follow platform theme |
| `light` | Force light theme |
| `dark` | Force dark theme |
The service persists the selected theme mode, falls back to `system` for invalid stored values, and preserves independent saves for host and nickname.
## 4. App Wiring
`ChanoraApp` becomes stateful enough to load and apply persisted theme mode. `_BetaHome` continues to load/save host and nickname through `UiPreferencesService`. UI controls for selecting theme mode are out of this slice unless already present; this slice provides persistence and app-level application.
## 5. Documentation Updates
After tests pass:
| Document | Update |
|---|---|
| `docs/implementation-status-2026-05-28.md` | Mark reducer scaffold statement stale/resolved and UI settings persistence implemented for SharedPreferences scope |
| `docs/release/dv-waiver-register.md` | Close or soften reducer waiver; keep event replay as P1 gap |
| `docs/verification/swe4-unit-verification-plan.md` | Record reducer test evidence and UI settings tests |
| `docs/verification/swe6-software-verification-plan.md` | Update state sync and UI settings DV status |
| `docs/architecture/sdd.md` | Record UI settings persistence design |
## 6. Validation
Run focused tests:
```text
cargo test -p chanora_state --locked
flutter test test/services/ui_preferences_service_test.dart
```
Run wider checks if touched app-shell behavior requires it:
```text
flutter test --exclude-tags e2e
```