Commit Graph
66 Commits
Author SHA1 Message Date
M57 b1c628ee24 Merge pull request #118 from Hy4ri/feat/issue-60-tool-json-parsing
feat(#60): human-readable tool JSON parsing in chat
2026-06-19 23:32:27 +03:00
M57 85dc48fcd6 Merge pull request #117 from Hy4ri/revert-pr-42
Revert "Merge pull request #42 from Hy4ri/feat/sessions-screen"
2026-06-19 23:24:36 +03:00
M57 7d50b5acd7 Merge pull request #42 from Hy4ri/feat/sessions-screen
feat: dedicated Sessions browser screen (Issue #40)
2026-06-19 23:14:40 +03:00
M57 ee97be50cb Merge pull request #113 from Hy4ri/refactor/issue-79-retrofit-error-handling
fix(#79): centralize retrofit error handling with NetworkResult
2026-06-19 23:03:21 +03:00
M57andM57 43df3c6e73 fix(#64): call wsClient.disconnect() directly in onCleared() instead of launching on cancelled scope (#94)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 22:18:04 +03:00
M57andM57 abcec14cb5 feat(#76): add testTag identifiers to composable elements for UI test targeting (#98)
Adds testTag modifiers to key interactive elements across the core
composables, enabling Espresso/Compose UI tests to locate them:

Navigation.kt  — 'nav_*' on bottom bar items, 'drawer_*' on drawer items
HermesScaffold — 'back_button', 'menu_button', 'refresh_button'
StateViews.kt  — 'loading_state', 'loading_spinner', 'error_retry_button',
                 'empty_state_action'
ChatScreen.kt  — 'chat_input', 'send_button', 'interrupt_button'

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 22:15:53 +03:00
M57andM57 cc52442be3 fix(#65): request POST_NOTIFICATIONS runtime permission on Android 13+ (#97)
* fix(#65): request POST_NOTIFICATIONS runtime permission on Android 13+

On API 33+ the manifest declaration alone is not enough — the app must
request POST_NOTIFICATIONS via ActivityResultContracts.RequestPermission
at runtime before it can show any notification, including the foreground
service notification from ChatNotificationService.

- Added rememberLauncherForActivityResult for POST_NOTIFICATIONS
- Added LaunchedEffect(Unit) that fires the permission request on first
  composition when running on API 33+ and permission is not yet granted
- The next lifecycle event (ON_STOP → NotificationHelper.start) picks
  up the permission if granted

* fix: add missing import android.os.Build for POST_NOTIFICATIONS permission check

* fix: add missing import for rememberLauncherForActivityResult (androidx.activity.compose)

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 22:15:47 +03:00
M57andM57 27a896c634 docs(#70): sync project structure in README with actual codebase (#96)
- Removed references to HermesControlApp.kt, EmptyState.kt, LoadingState.kt
  (none of these files exist)
- Replaced with StateViews.kt, SharedComponents.kt
- Added data/ws/ (WebSocket TUI Gateway layer)
- Added notification/ (ChatNotificationService foreground service)
- Added util/ (CronExpressionFormatter)
- Added missing theme files: HermesStatusColors.kt, Spacing.kt, Shapes.kt
- Added settings/ and connect/ screen directories

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 22:15:41 +03:00
M57andM57 451412851f fix(#69): replace mipmap launcher icon with proper notification vector drawable (#95)
Notification smallIcon must use a simple mono vector drawable, not a
launcher mipmap. Launcher icons render broken in the status bar on
many devices.

- Added app/src/main/res/drawable/ic_notification.xml (simple info icon)
- Updated both showReplyNotification and buildForegroundNotification to
  reference R.drawable.ic_notification instead of R.mipmap.ic_launcher

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 22:15:36 +03:00
M57andM57 607a1b0439 fix(#73): surface clear errors for malformed pairing strings instead of silent fallback
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 21:09:19 +03:00
M57andM57 6375e3fa1d fix(#74): mark unparseable cron expressions instead of silently returning raw
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 20:52:44 +03:00
11d97a2d5f fix(#75): replace non-null assertion on simpleName with safe mapNotNull
* ci: prevent concurrency cancellation on rapid main merges

The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.

Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).

* refactor(#82): remove dead SkeletonList/SkeletonCard composables

* fix(#72): echo server's WebSocket close code instead of hardcoded 1000

* fix(#75): replace non-null assertion on simpleName with safe mapNotNull

---------

Co-authored-by: ronia <ronia@m57>
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 20:48:48 +03:00
16e5973dc9 fix(#72): echo server's WebSocket close code instead of hardcoded 1000
* ci: prevent concurrency cancellation on rapid main merges

The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.

Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).

* refactor(#82): remove dead SkeletonList/SkeletonCard composables

* fix(#72): echo server's WebSocket close code instead of hardcoded 1000

---------

Co-authored-by: ronia <ronia@m57>
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 20:45:42 +03:00
24847cf0d6 refactor(#82): remove dead SkeletonList/SkeletonCard composables
* ci: prevent concurrency cancellation on rapid main merges

The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.

Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).

* refactor(#82): remove dead SkeletonList/SkeletonCard composables

---------

Co-authored-by: ronia <ronia@m57>
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 20:45:37 +03:00
M57andronia b0ba1b7f00 ci: prevent concurrency cancellation on rapid main merges
The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.

Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).

Co-authored-by: ronia <ronia@m57>
2026-06-19 20:33:36 +03:00
M57 8519add306 Merge pull request #87 from Hy4ri/fix/release-source-archive-dir
ci(release): add missing mkdir -p release before source archive
2026-06-19 20:25:32 +03:00
M57 a026e750f3 Merge pull request #86 from Hy4ri/refactor/issue-81-remove-commandpanel
[refactor] Delete dead CommandPanel component (#81)
2026-06-19 20:21:00 +03:00
M57 637cbd73f5 Merge pull request #85 from Hy4ri/refactor/issue-80-proguard-coroutines
[refactor] Remove blanket coroutines keep rule from ProGuard (#80)
2026-06-19 20:19:43 +03:00
M57andronia a8600668c2 docs: add AGENTS.md with build, style, and workflow guidance for AI coding agents (#84)
Co-authored-by: ronia <ronia@m57>
2026-06-19 20:17:55 +03:00
M57 209ce331b8 Merge pull request #83 from Hy4ri/refactor/issue-77-remove-workmanager
[refactor] Remove unused WorkManager dependency (#77)
2026-06-19 20:13:37 +03:00
M57 028b490fd3 Merge pull request #62 from Hy4ri/fix/issue-45-ws-stuck
Fix WebSocket connection stuck on subsequent launches (Issue #45)
2026-06-19 20:05:36 +03:00
M57 6b9bab9c25 Merge pull request #61 from Hy4ri/fix/issue-59-keyboard-gap
Consume window insets in NavDisplay to eliminate keyboard gaps (Issue #59)
2026-06-19 19:49:56 +03:00
M57 379c9e07e7 Merge pull request #57 from Hy4ri/fix/issue-44-chat-search
Fix chat search breaking vertical scrolling and layout (Issue #44)
2026-06-19 19:41:20 +03:00
M57 95b6106b4c Merge pull request #55 from Hy4ri/feat/source-archive-release
feat: attach source code archive to release assets (closes #54)
2026-06-19 19:00:48 +03:00
M57 77ed9c9120 Merge pull request #56 from Hy4ri/fix/issue-51-cron-human-readable
Implement human-readable cron schedule formatter (Issue #51)
2026-06-19 19:00:40 +03:00
M57 3e093aaa72 Merge pull request #53 from Hy4ri/fix/issue-47-reorder-nav-items
feat: add bottom nav reordering in settings (closes #47)
2026-06-19 18:46:09 +03:00
M57 9d5e46e769 Merge pull request #52 from Hy4ri/fix/issue-48-chat-input-space
fix: remove space between bottom bar and chat input field (closes #48)
2026-06-19 18:45:07 +03:00
M57 a7fcfd4088 Merge pull request #50 from Hy4ri/fix/issue-49-always-show-bottom-bar
fix: keep bottom navigation bar always visible (closes #49)
2026-06-19 18:36:26 +03:00
M57 2e56e2d9da Merge pull request #46 from Hy4ri/fix/issue-43-bottom-navbar
fix: bottom navbar updates dynamically (closes #43)
2026-06-19 18:29:21 +03:00
M57 1b188b6b31 Merge pull request #41 from Hy4ri/fix/release-signing
fix: sign release APKs with stable keystore from secrets
2026-06-19 17:44:19 +03:00
M57andronia 59e0d4f817 feat: search within chat history (Issue #31) (#39)
- Add search icon toggle in top bar with animated search field
- Real-time message filtering on search query match
- Match counter with previous/next navigation arrows
- Case-insensitive text highlighting in user and assistant bubbles
- Dismiss search restores full message list
- Empty state for no search results

Co-authored-by: ronia <ronia@m57>
2026-06-19 16:35:07 +03:00
M57 fb605d1bc0 feat: clickable URLs and long-press copy in chat bubbles (Issues #29, #30) (#36)
Add clickable URL detection with primary color + underline styling and browser launch via UriHandler. Add long-press to copy raw message text to clipboard for both user and assistant chat bubbles.

Fixes #29, Fixes #30
2026-06-19 14:16:56 +03:00
M57 2cfe1f71fc fix: bottom navigation bar customization not reflected in NavigationBar (Issue #37)
fix: bottom navigation bar customization not reflected in NavigationBar (Issue #37)
2026-06-19 13:36:25 +03:00
M57 66da56ca24 feat: connection status indicator in drawer (Issue #28)
feat: connection status indicator in drawer (Issue #28)
2026-06-19 12:40:32 +03:00
M57 27e513f0a3 feat: show app version in Settings screen (Issue #32)
feat: show app version in Settings screen (Issue #32)
2026-06-19 12:39:20 +03:00
M57 0f6625c66e fix: migrate deprecated Compose/Room APIs (Issues #25, #26, #27)
fix: migrate deprecated Compose/Room APIs (Issues #25, #26, #27)
2026-06-19 12:38:16 +03:00
M57 0038606c05 feat: add connection status indicator (Issue #28)
Adds a colored dot next to the '⚡ Hermes' brand header in the
navigation drawer:
  - Green: connected
  - Yellow: connecting / reconnecting
  - Red: disconnected

Uses a StateFlow on HermesWsClient that tracks the WebSocket
connection lifecycle and is collected in the drawer header.
2026-06-19 12:07:44 +03:00
M57 12c60dce16 feat: show app version in Settings screen (Issue #32)
Displays version name and build code at the bottom of the Settings
screen using BuildConfig.VERSION_NAME and VERSION_CODE.
2026-06-19 12:03:37 +03:00
M57 1c1961426a fix: migrate deprecated APIs (Issues #25, #26, #27)
- Icons.Filled.ArrowBack/ListAlt → Icons.AutoMirrored.Filled (Issue #25)
- ScrollableTabRow → PrimaryScrollableTabRow (Issue #26)
- fallbackToDestructiveMigration() → fallbackToDestructiveMigration(false) (Issue #27)
2026-06-19 12:02:07 +03:00
M57 0fa61148a3 Create LICENSE 2026-06-18 22:28:27 +03:00
M57 34df198c4d feat: configurable bottom navigation bar (Issue #23)
## What
Adds a new Navigation Bar section to Settings where users can customize which items appear in the bottom navigation bar.

## Features
- Pick from any of the 19 drawer screens (max 5 items)
- Remove items with a close/delete button
- Items persist via SharedPreferences (CSV format)
- Invalid names are silently dropped
- Existing screens and auth-aware filtering respected

Closes #23
2026-06-18 21:16:21 +03:00
M57andM57 837678c590 fix(#21): remove double spacing above top bar and below chat input (#22)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 20:36:56 +03:00
M57andM57 7048985e06 ci(#19): add wrapper validation, remote build cache, and drop dependency-submission (#20)
* ci(#19): add wrapper validation, remote build cache, and dependency submission

* ci(#19): remove flaky dependency-submission job — core CI is enough

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 19:32:19 +03:00
M57andM57 b01e6d2fee feat(#17): replace default icons with custom Hermes-branded adaptive icon (#18)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 19:16:27 +03:00
M57andM57 7c4735ed1c docs(#15): add comprehensive README (#16)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 19:09:46 +03:00
M57andM57 4892572bcc feat(#13): add pull-to-refresh to HermesScaffold (#14)
- HermesScaffold: added isRefreshing param, wraps content in PullToRefreshBox when onRefresh is set
- All 16 data screens: pass isRefreshing = state.isLoading
- Initial loading only shows full-screen LoadingState when data is empty (no spinner over existing content)
- AchievementsScreen already had the correct pattern, just needed isRefreshing param

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 18:58:08 +03:00
M57 d37de1d223 feat(#11): make HermesScaffold title a @Composable slot, convert ChatScreen (#12)
- HermesScaffold: title now @Composable () -> Unit (M3-idiomatic)
- HermesScaffold: added modifier and snackbarHost parameters
- Updated all 18 callers from title="X" to title={Text("X")}
- ChatScreen: converted from raw Scaffold to HermesScaffold
- Removed unused Menu icon import (ktlint fix)
2026-06-18 18:43:45 +03:00
M57andM57 7c27ca624c refactor(#5): convert SettingsScreen to use HermesScaffold
* refactor(#5): convert SettingsScreen to use HermesScaffold

Replace raw Material3 Scaffold + manual TopAppBar with the shared
HermesScaffold wrapper. Removes ~30 lines of duplicated boilerplate
and gives SettingsScreen scroll-aware top bar behavior.

Closes #5

* fix(#5): restore ExperimentalMaterial3Api import for SegmentedButton

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 18:00:51 +03:00
M57andM57 a36be6e432 fix(#4): replace private EmptyState with shared component in ChatScreen
Remove ~30 lines of duplicated code by using the shared EmptyState
from ui/common/StateViews.kt instead of the private version.

Closes #4

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 17:51:52 +03:00
M57 d90e36974c feat: Material You design system rewrite — Phases 1–4
Complete UI/UX rewrite of hermes-mobile with Material You design system.

Phase 1: Color/Theme/Type/Shapes/Spacing/Motion tokens
Phase 2: HermesScaffold, StateViews (skeleton/empty/error), SearchBar, StatusBadge, ToggleRow
Phase 3: Bottom nav + drawer navigation with NavigationController
Phase 4: All 17 screens rewritten with HermesScaffold and Material You patterns

Closes PR #3.
2026-06-18 16:34:44 +03:00
M57 8d3894b7c7 fix(ci): add contents:write permission for GitHub Release creation 2026-06-18 03:31:00 +03:00
M57andM57 5130c2e308 feat(chat): message persistence + background notifications (#2)
* feat(chat): message persistence + background notifications

Two critical gaps for daily-driver use:

1. Message persistence (Room database):
   - New ChatMessageEntity / ChatMessageDao / HermesDatabase layer
   - Every user message, streaming token, tool result, and assistant
     reply is written to a local Room DB as it arrives
   - switchSession loads cached messages instantly from Room before
     the REST call returns — no more blank screen while loading
   - Session resume also loads from cache first
   - Messages survive app kills, process death, and configuration
     changes
   - ChatViewModel changed from ViewModel() to AndroidViewModel(app)
     to access the database context

2. Background notifications (foreground service):
   - ChatNotificationService keeps the WebSocket alive while the app
     is backgrounded and posts a high-priority notification when a
     MessageComplete or ClarifyRequest event arrives while the app
     is not in the foreground
   - Lifecycle wired via LifecycleEventObserver in ChatScreen:
     ON_STOP starts the service, ON_START stops it
   - Tapping the notification returns to the chat
   - New permissions: POST_NOTIFICATIONS, FOREGROUND_SERVICE,
     FOREGROUND_SERVICE_DATA_SYNC

Build changes:
   - Added Room 2.7.1 + KSP to version catalog
   - Added WorkManager 2.10.0 (reserved for future polling fallback)
   - Updated ChatViewModelTest to mock HermesDatabase + use
     AndroidViewModel constructor

* fix(build): use KSP 2.3.9 (new versioning scheme for Kotlin 2.3.x)

* fix(build): add Room schemaDirectory (required by Room Gradle plugin)

* fix(compile): add else branch to WsEvent when expression (exhaustiveness)

* ci(release): add release workflow + R8 minification + proguard rules

New .github/workflows/release.yml:
- Triggers on tag pushes (v*) and manual workflow_dispatch
- Runs full CI (ktlint + lint + test) before building
- Builds assembleRelease with version injection from tag
- Creates a GitHub Release with auto-generated changelog
- Attaches the release APK to the release

Build changes:
- Release builds now use R8 minification (isMinifyEnabled=true)
  and resource shrinking (isShrinkResources=true) for smaller APKs
- versionCode/versionName injectable via -PversionCode / -PversionName
  Gradle properties (passed by CI from the git tag)
- Local dev builds default to versionCode=1, versionName=1.0-dev

ProGuard rules:
- Keep all data model classes (Gson reflection)
- Keep Room entities and RoomDatabase subclasses
- Keep OkHttp/Retrofit interface methods
- Suppress warnings for Compose, Security Crypto, Coroutines

* feat(ci): release workflow + proguard rules + test fix

Release workflow (.github/workflows/release.yml):
  - Triggers on tag push (v*) or manual workflow_dispatch
  - Two jobs: ci (ktlint+lint+test) → build-release
  - Builds a minified, shrunk release APK with version from tag
  - Creates a GitHub Release with auto-generated release notes
  - Attaches the APK as a release asset

Build config:
  - versionCode/versionName now injected from CI via -P flags
  - Release build enables isMinifyEnabled + isShrinkResources
  - proguard-rules.pro: keep rules for Gson models, Room entities,
    OkHttp/Retrofit interfaces, Coroutines, Compose, BuildConfig

Test fix:
  - ChatViewModelTest: use coEvery for suspend DAO methods instead
    of every (compile error — suspend functions need coEvery)
  - Added stubs for all suspend DAO methods used by the ViewModel

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 03:23:38 +03:00
7daf73ce0d feat(ui): modern theme, sectioned drawer, shared components (v1) (#1)
* feat(ui): modern theme, sectioned drawer, shared components (v1)

Foundation for the modern redesign — fixes the biggest UX problems
(that vague "better ui ux" ticket) without rewriting all 20 screens
at once. This puts the infrastructure in place and modernizes the
highest-traffic screens.

Theme (
  - Disable Material You dynamic color by default — brand identity
    (Voltage Purple) wins. Can opt back in via HermesControlTheme.
  - 3-step elevation ladder (surface / surfaceVariant / surfaceHigh)
    with subtle blue tint so dark mode stops reading as flat black.
  - Full Material 3 type hierarchy — every style explicitly tuned,
    tighter letter spacing, comfortable line heights.
  - New Shapes.kt (xs→xl rounded corner tokens) and Spacing.kt
    (xs/sm/md/lg/xl/xxl spacing tokens) — no more ad-hoc dp values.

Navigation (
  - Sectioned drawer: 17 items were dumped at one indent level with
    duplicate icons (Channels & Chat both used the Chat icon!). Now
    grouped into 4 logical sections — Converse / Automate / Configure
    / Inspect — with section headers and sane per-item icons.
  - Settings added to the drawer (was previously hidden behind
    ChatScreen's gear icon — bad IA).
  - gesturesEnabled check collapsed from an 18-term OR chain into a
    single set-membership test.

Common components (
  - HermesScaffold: shared drawer-aware Scaffold + TopAppBar with
    slots for refresh / actions. Removes the per-screen duplication
    seen in SkillsScreen, CronJobsScreen, ChannelsScreen, etc.
  - StateViews: LoadingState / ErrorState / EmptyState / list content
    padding tokens — every list screen now renders loading/error/
    empty identically instead of ad-hoc per screen.

Modernized screens:
  - ConnectScreen: hero brand badge (was a lone "⚡" glyph), pairing
    as the primary path, manual token/host/port collapsed behind a
    toggle (was always-visible clutter), Card-in-Card nesting removed.
  - SkillsScreen: now uses HermesScaffold + EmptyState + ErrorState +
    LoadingState. Adds a search field, leading icon per row, and a
    category sublabel.

Follow-up commits will migrate the remaining ~15 list screens onto
HermesScaffold; this commit stops at the foundation + the two screens
that see the most daily use.

Verified: ktlint clean across the entire tree.

Co-Authored-By: Ronia <ronia@m57>

* fix(imports): add missing safeDrawingPadding + dp imports

CI caught three compile errors that ktlint doesn't flag (unresolved
references are not lint errors, they're compile errors):

- Navigation.kt:239  — Modifier.safeDrawingPadding() import was
  dropped when the file was rewritten for the sectioned drawer.
- SkillsScreen.kt:117 — `16.dp` and `12.dp` references had no
  `androidx.compose.ui.unit.dp` import after the file was rewritten
  onto HermesScaffold.

No code changes — pure import fixes.

Co-Authored-By: Ronia <ronia@m57>

* ci: split monolithic job into 4 parallel jobs + summary

Before — one job ran ktlint → lint → test → build sequentially. If
ktlint failed you waited ~30s to find out, and the test/build stages
never even started (no useful artifacts uploaded on partial failure).

After — 4 jobs run in parallel on every push and PR:
  1. ktlint          (fast feedback, ~30s)
  2. android-lint    (parallel, includes compileDebugKotlin)
  3. unit-tests      (parallel, JUnit 5 + mockk)
  4. build           (gated by 1+2+3, only runs if all pass)

Plus a 5th `ci-summary` job that aggregates results so branch
protection only needs a single status check configured.

Other improvements:
- concurrency cancel-in-progress — new commits on the same ref
  cancel the stale run instead of piling up.
- timeout-minutes on every job — no more hung runners burning minutes
  on a stuck Gradle daemon.
- workflow_dispatch — can trigger manually from the Actions tab.
- Per-job artifact uploads with `if: always()` — reports upload even
  when the job fails, so you can download test/lint reports to debug.
- Removed the monolithic "verification-reports" artifact that tried
  to upload three unrelated paths and failed silently with "No files
  were found" when an early step bailed.

Co-Authored-By: Ronia <ronia@m57>

* ci: silence ktlint artifact-not-found annotation

Adds `if-no-files-found: ignore` to the ktlint artifact upload step
because ktlint 1.2.1 does not produce HTML reports on success — only
on failure. The previous config `if-no-files-found: warn` (default)
emitted a noisy annotation on every green run.

Co-Authored-By: Ronia <ronia@m57>

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Ronia <ronia@m57>
2026-06-18 02:59:43 +03:00
M57 925cc32de9 fix: guard doctor failure Log.w with BuildConfig.DEBUG 2026-06-18 02:33:22 +03:00
M57 fcaab0e3f0 docs: document LAN-only http:// assumption in AuthManager 2026-06-18 02:33:22 +03:00
M57 0874c5f0f0 docs: update test counts from 28 to 123 with per-file breakdown 2026-06-18 02:33:21 +03:00
M57 44aa7ba84b fix: key ChannelsScreen items by platform.id to preserve form state 2026-06-18 02:32:05 +03:00
M57 31bf15b561 fix: make KanbanBoardsResponse.current nullable for Gson safety 2026-06-18 02:30:27 +03:00
M57 73126dc94b fix: guard goBack() against size-1 stack underflow 2026-06-18 02:30:27 +03:00
M57 a28d53127f test: update switchSession asserts for B3 isLoading fix
loadSessionMessages now resets isLoading=false on its error branch
(was previously stuck at true). Test was implicitly relying on the bug.
2026-06-18 02:25:45 +03:00
M57andRonia a0b4ce8dc0 fix(security,race,ux): close 6 high-priority bugs (B1–B6)
Wave 1 — CRITICAL (token + PII leak):
  B1 (kanban t_afc1d26f): Auth token leaked to logcat via
    HttpLoggingInterceptor.Level.BODY combined with the authInterceptor
    that adds `Authorization: Bearer *** on every REST call. Enabled
    buildConfig=true in app/build.gradle.kts and gate Level.BODY on
    BuildConfig.DEBUG (Level.NONE in release). ApiClient.kt:44-56.
  B2 (kanban t_8884db16): Chat prompts + AI replies leaked to logcat
    via unguarded Log.d in HermesWsClient. Gated the 3 PII-critical
    sites (line 104 outgoing JSON which contains user `text`, line ~123
    connecting URL whose query string carries the auth token, line ~181
    incoming WS payload with AI reply tokens and tool output) with
    BuildConfig.DEBUG. Log.i/.w/.e left unguarded as intentional ops signals.

Wave 2 — HIGH (races + thread-pattern):
  B3 (kanban t_33da8a97): switchSession fired SESSION_RESUME (WS) and
    loadSessionMessages (REST) in parallel. The REST success path did an
    unconditional `_uiState.update { it.copy(messages = chatMessages) }`
    that stomped any in-flight streaming tokens appended via
    WsEvent.MessageToken. ChatViewModel.loadSessionMessages now preserves
    messages with isStreaming=true by re-appending them to the freshly
    loaded history. Bonus side-fix discovered in same function: isLoading
    was never set to false on success or error branches — now explicitly
    cleared on all exit paths.
  B4 (kanban t_2b834d90): HermesWsClient.scheduleReconnect used a raw
    Thread{} with Thread.sleep + a non-atomic check-then-openSocket()
    (TOCTOU). Two reconnect threads spawned by a rapid onFailure/onClosed
    cycle could both pass the gate and both call openSocket(), the second
    overwriting `webSocket` without closing the first — orphaned socket
    listener leak. Flaky networks multiplied daemon thread count. Replaced
    with a single cancellable coroutine Job keyed to wsScope
    (Dispatchers.IO + SupervisorJob). disconnect() now cancels any pending
    reconnect so a delayed openSocket() can't fire after explicit close.

Wave 3 — MEDIUM (UX gaps + persistence):
  B5 (kanban t_2322818d): LogsScreen rendered a blank black Box when
    state.logs was empty && !isLoading && errorMessage == null. Added an
    explicit empty-state branch rendering "No logs available" in
    onSurfaceVariant, centered. Mirrors sibling pattern in
    PairingScreen.kt:200-219 and WebhooksScreen.kt:171-190.
  B6 (kanban t_86e9be9b): Theme preference was in-memory only — lost on
    every cold start. Verification confirmed three gaps: (1)
    SettingsViewModel.loadSettings() didn't restore themePreference from
    storage; (2) SettingsViewModel.save() didn't persist it; (3)
    MainActivity called `HermesControlTheme {}` with no themePreference
    arg so even if storage had worked, the app wouldn't apply it at
    startup. Added getThemePreference/setThemePreference in AuthManager
    (EncryptedSharedPreferences round-trip, validate stored enum name with
    runCatching to handle forward-incompatible strings), wired
    SettingsViewModel init + save, and passed the stored preference to
    HermesControlTheme(themePreference = AuthManager.getThemePreference())
    at startup.

Verification:
  - ktlintCheck: BUILD SUCCESSFUL in 7s (ktlint app/src/**/*.kt — clean)
  - ./gradlew test: NOT RUN on this host (no Android SDK at
    $ANDROID_HOME); deferred to the CI workflow at
    .github/workflows/android.yml. Same limitation applies to
    assembleRelease. Symbol-resolution reasoning: app/build.gradle.kts:10
    declares namespace=com.m57.hermescontrol so AGP auto-generates the
    BuildConfig class at package com.m57.hermescontrol — matches the new
    imports in ApiClient.kt and HermesWsClient.kt.

Kanban tracking (native Hermes kanban, board slug "hermes-mobile"):
  B1 t_afc1d26f  done   - Auth token leak via Level.BODY
  B2 t_8884db16  done   - Chat PII via unguarded Log.d
  B3 t_33da8a97  done   - switchSession race stomps streaming tokens
  B4 t_2b834d90  done   - WS reconnect TOCTOU + unbounded Thread{}
  B5 t_2322818d  done   - LogsScreen missing empty-state
  B6 t_86e9be9b  done   - Theme preference not persisted
  B7 t_74f3b383  ready  - (deferred) NavigationController.goBack underflow
  B8 t_39a149f0  ready  - (deferred) KanbanBoardsResponse.current nullable
  B9 t_4b1465cf  ready  - (deferred) E2eIntegrationTest base64 no-op via Mockk
  B10 t_81ef4203 ready  - (deferred) ChannelsScreen form state on scroll
  B11 t_3325498b ready  - (deferred) TEST_READY/TEST_INFRA stale (28 vs 123)
  B12 t_674007f5 ready  - (deferred) Hardcoded http:// (LAN assumption)
  B13 t_179009d5 ready  - (deferred) SystemViewModel silent doctor catch

Total: 8 files changed, +144 insertions, -26 deletions.

Co-Authored-By: Ronia <ronia@hermes>
2026-06-18 02:17:11 +03:00
M57 6152ee42b2 fix(data,ui): update plugin info model, API service, and plugins viewmodel 2026-06-16 17:31:33 +03:00
M57 4c572509ec fix(system,kanban): make doctor optional and render dynamic kanban columns 2026-06-16 10:13:11 +03:00
M57 a994c7d5db test: fix malformed JSON in HermesApiServiceTest cron job stub 2026-06-16 01:29:39 +03:00
M57 8b4f5f91e0 style: fix ktlint violations (unused imports + line length) 2026-06-16 01:22:48 +03:00
M57 72e3a06047 ci: install ktlint before running ktlintCheck 2026-06-16 01:19:14 +03:00