# TASK-031: "커밋" label rename, EUC-KR decode fallback, HTTP timeout/connection-pooling fix Status: implemented; all required checks pass; live-installed, awaiting the human product owner's confirmation Owner: Claude Reviewer: Codex ## Outcome Three small, independent fixes reported live in the same testing session: the "히스토리" (History) tab/button label is now "커밋" (Commit), matching the human product owner's own correct description of what that screen actually shows; `.cpp`/`.h` (and any other legacy-encoded) source files that use EUC-KR instead of UTF-8 — common in older or Windows-authored Korean C/C++ codebases — now render instead of being rejected as `UnsupportedEncoding`; and a large repository's commit history (and, more generally, any slow-but-working request against this app's home-NAS-hosted Gitea instance) no longer gets misreported as "서버에 연결할 수 없습니다. 네트워크를 확인해 주세요." (can't reach the server) purely because it took longer than the previous 10-second default timeout to answer. ## Scope - Included: `app/src/main/res/values/strings_branchdetail.xml`, `strings_codebrowser.xml`, `strings_history.xml` (label/wording changes only); `feature/codebrowser/content/SourceContentClassifier.kt` (+ matching test) for the EUC-KR fallback; `core/network/HttpTransport.kt` for the shared, longer-timeout `OkHttpClient`. - Excluded: No change to the actual commit-history or file-browsing functionality themselves — this batch is a label, a decode fallback, and a network-client tuning change, not new features. No attempt to distinguish "genuinely offline" from "slow but reachable" in the error message itself (see Known limitations) — the fix is to stop timing out prematurely, not to redesign the error taxonomy. ## Design notes - **Label rename**: the human product owner directly confirmed, then asked for, this: the "History" tab (branch detail) and "History" button (code browser, jumps to a file's commit history) both said "히스토리"; both are now "커밋" to match what a Gitea user actually expects that label to mean. `history_permission_denied`'s sentence ("이 저장소의 히스토리를 볼 권한이 없습니다") was reworded to "커밋 내역을 볼 권한이 없습니다" for the same reason — it's the only other user-facing string in the app that used the word "히스토리" on its own (a full res/values scan confirmed these were the only three occurrences). Internal identifiers (`HistoryScreen`, `HistoryViewModel`, `history_*` resource-name prefixes, the `feature/history` package) were left alone — renaming those is a much larger, purely-internal refactor with no user-visible benefit, and touching them risked the exact kind of unrelated churn this session's standing "don't change functionality" constraint warns against. - **EUC-KR fallback, not a blind fallback**: the classifier's own design principle — decode with `CodingErrorAction.REPORT`, never `REPLACE`, so a byte sequence that isn't cleanly one specific encoding is refused rather than rendered as mojibake — is preserved exactly; a second charset is now tried with the same strict decode, not a permissive one. EUC-KR specifically (not the more complete MS949/CP949 superset some of the same files may technically use) because Android's own `Charset` documentation lists EUC-KR as present on every device via bundled ICU data, while CP949/MS949 support is not guaranteed — using it would have risked compiling and passing this project's host-JVM unit tests while throwing `UnsupportedCharsetException` at runtime on the actual device, which is exactly the kind of gap this project's tests wouldn't have caught without a live device check. A byte sequence invalid in both UTF-8 and EUC-KR (e.g. `0xFF` as a lead byte, valid in neither) still correctly falls through to `UnsupportedEncoding` — this is not a catch-all "always render something" fallback, it's specifically shaped around the one legacy encoding this app's real, live-tested repositories actually turned out to use. - **HTTP timeouts and connection sharing, one root cause for two symptoms**: every one of the app's ~8 `OkHttpTransport()` call sites (one per feature's `ViewModel.Factory`, plus `AuthSession`) constructed its own fresh, default-configured `OkHttpClient()` — default 10-second connect/read/ write timeouts. Traced directly from the live bug report (commit listing on a real, larger-history repository — visible on device as "LGD-P9-CLCI-4K" — reliably produced the generic offline error) through `GiteaRepositoryBrowserApi.authenticated`/`GiteaHistoryApi.get`, both of which catch `IOException` broadly (this necessarily includes `SocketTimeoutException`) and wrap it as `ApiFailure.Network`, which every feature's error mapping renders identically to a truly offline device. This app's actual Gitea host is a home Synology NAS, not a datacenter service, so a legitimately slow-but-working response — a large commit history walk, a directory listing with many entries (the second, related complaint: loading feeling too slow as file counts grow) — was being cut off and misreported before it had a real chance to finish. One shared `OkHttpClient` (15s connect / 30s read / 15s write, versus the previous unconfigured 10s/10s/10s) is now `OkHttpTransport`'s default, replacing the previous `OkHttpClient()` bare default: longer timeouts give a slow response room to actually complete instead of erroring out early, and because it's now one shared instance (not ~8 independently-constructed ones), OkHttp's own connection pool is reused across every feature's calls to the same host within a session — a second call to the same Gitea host during the same app session can reuse an already-open TCP/TLS connection instead of paying a fresh handshake, which is a genuine (not just perceived) latency win on top of the timeout fix. ## Acceptance criteria - [x] The branch-detail tab and the code-browser's "view this file's history" button both read "커밋" instead of "히스토리". - [x] `history_permission_denied`'s message no longer contains the word "히스토리". - [x] A `.cpp`/`.h` (or any text) file encoded in EUC-KR, not UTF-8, renders as text instead of `UnsupportedEncoding`. - [x] A byte sequence invalid in both UTF-8 and EUC-KR still correctly classifies as `UnsupportedEncoding` — the fallback doesn't swallow genuinely unrecognized encodings. - [x] `OkHttpTransport`'s default client now uses longer, explicit timeouts (15s connect / 30s read / 15s write) instead of an unconfigured default `OkHttpClient()`, and is shared (one instance, one connection pool) across every call site instead of one fresh client per call site. - [x] Build, unit tests, and lint pass. - [ ] Live confirmation from the human product owner that the "LGD-P9-CLCI-4K" repository's commit history now loads instead of failing, that `.cpp`/`.h` files open correctly, and that the "커밋" label reads correctly — the build has been reinstalled on the device but not yet re-tested against those exact cases in this session. ## Required checks ```text ./gradlew compileDebugKotlin compileDebugUnitTestKotlin ./gradlew testDebugUnitTest ./gradlew assembleDebug lintDebug ``` ## Evidence - `strings_branchdetail.xml`/`strings_codebrowser.xml`: `히스토리` → `커밋`. - `strings_history.xml`: `history_permission_denied` reworded to say "커밋 내역". - `SourceContentClassifier.kt`: `decodeStrictUtf8` replaced by `decodeText` (tries UTF-8, then EUC-KR, both strict) built on a new shared `decodeStrict(bytes, charset)` helper; new `EUC_KR` charset constant. - `SourceContentClassifierTest.kt`: two new tests — EUC-KR-encoded Korean text now classifies as `Text` with the correctly decoded string; a byte sequence invalid in both UTF-8 and EUC-KR still classifies as `UnsupportedEncoding`. The two pre-existing "malformed UTF-8" tests were checked (not just left alone) to confirm their specific byte sequences are also invalid under EUC-KR's own lead/trail-byte rules, so they still correctly exercise the "both attempts fail" path rather than accidentally now succeeding via the new fallback. - `HttpTransport.kt`: new private `sharedOkHttpClient` (lazy, 15s/30s/15s timeouts), now `OkHttpTransport`'s default `client` parameter, replacing the previous bare `OkHttpClient()`. - `./gradlew compileDebugKotlin compileDebugUnitTestKotlin`: BUILD SUCCESSFUL. - `./gradlew testDebugUnitTest`: BUILD SUCCESSFUL, all unit tests passing (including the two new ones above). - `./gradlew assembleDebug lintDebug`: BUILD SUCCESSFUL, 0 lint errors. - Installed on the human product owner's device over wireless `adb` (`adb install -r`, streamed install succeeded); not yet re-tested live against the specific repository/files that originally reported these bugs in this session. ## Known limitations - The error message itself still doesn't distinguish "genuinely offline" from "reachable but slow" — a request that's still too slow even at the new 30-second read timeout (or a device that's truly offline) both still render as the same "서버에 연결할 수 없습니다" message. The fix here is to stop the common case of a merely-slow-but-working NAS response from being misclassified, not a full error-taxonomy redesign. - EUC-KR only, not the larger CP949/MS949 superset — a file using a Hangul syllable or symbol only representable in CP949's extended completion range (not present in the older EUC-KR/KS X 1001 set) will still classify as `UnsupportedEncoding`. This was a deliberate choice given Android's charset-availability guarantees (see Design notes), not an oversight. - No caching or ETag/conditional-request support was added — the connection-pool sharing reduces handshake overhead for repeated calls to the same host, but each call still does a full request/ response round trip; a directory or commit list re-opened a second time still re-fetches from scratch. - Live confirmation against the exact repository and files that originally reported these bugs is still pending in this session (see Acceptance criteria). ## Review findings None yet — no Codex review pending (Codex retired, 2026-09-29).