Commit Graph
31 Commits
Author SHA1 Message Date
M57andM57 2a9d4fa5be feat(#371): connection profiles full management (add, edit, delete) (#393)
* feat(#371): add full connection profiles CRUD (add/edit/delete)

* fix(#371): add missing TextButton import

* fix(#371): move Save button to header so it's visible on all settings tabs

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-26 08:07:30 +03:00
M57andM57 8d3d8a11e8 fix(#391): resolve 5 deprecation warnings (#392)
* fix(#391): resolve deprecation warnings

- Icon: Icons.Filled.ArrowForward → Icons.AutoMirrored.Filled.ArrowForward (KanbanScreen)
- Room: fallbackToDestructiveMigration() → fallbackToDestructiveMigration(false) (HermesDatabase)
- Tab: ScrollableTabRow → PrimaryScrollableTabRow (SettingsScreen)
- Clipboard: LocalClipboardManager → LocalClipboard + coroutine-scoped setText (ChatBubble: UserBubble, AssistantBubble, ToolBubble, CopyButton)
- ClickableText: pushStringAnnotation → pushLink(LinkAnnotation.Url), ClickableText → Text with native link handling (ChatBubble: RichText)

* fix(#392): replace deprecated setText with setClipEntry for new Clipboard API

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-26 05:25:01 +03:00
M57andM57 3399880621 perf(#281): fix PERF-13/14/15/16 — main-thread settings, 100Hz polling, leaked scope, WS disconnect (#359)
* perf(#281): fix PERF-13/14/15/16 — main-thread settings load, 100Hz polling, leaked scope, WS disconnect

* fix(#281): fix SettingsViewModelTest failures from async init dispatcher

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-25 20:39:52 +03:00
M57andM57 9297413b2a refactor(#285): extract section composables from SettingsScreen and ChatScreen (#358)
* refactor(#285): extract ConnectionSection from SettingsScreen

* refactor(#285): extract TestResultCard, SaveIndicator, SettingsActionButtons

* refactor(#285): extract BehaviorSection from SettingsScreen

* refactor(#285): extract AppearanceSection from SettingsScreen

* refactor(#285): extract ChatSection from SettingsScreen

* refactor(#285): extract NavBarSection from SettingsScreen

* refactor(#285): extract AboutSection from SettingsScreen

* refactor(#285): extract ChatLifecycleEffects from ChatScreen

* refactor(#285): extract ChatTopBanner from ChatScreen

* refactor(#285): extract ChatMessageList, ChatLoadingOverlay, ChatScrollToBottomFab

* fix(#285): use fully-qualified ButtonDefaults, BoxScope receiver for ChatScrollToBottomFab

* fix(#285): add BoxScope import

* fix(#285): pass isLoading as param instead of accessing StateFlow.value

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-25 20:36:21 +03:00
M57andM57 f190b67ee6 fix(#290): code health — magic colors, flag params, silent catch (#353)
* fix(#290): use LocalHermesStatusColors instead of magic colors (CODE-33)

* fix(#290): replace boolean-flag params with sealed NavIcon class (CODE-37)

* fix(#290): resolve compilation errors — Color import + nullable NavIcon params

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 21:26:32 +03:00
M57andM57 740b4fe601 Refactor: Consolidate navigation items into ScreenRegistry (#310)
* Refactor: Consolidate navigation items into ScreenRegistry

* fix(#310): remove unused imports, fix ktlint import ordering

* feat(#289): add ScreenRegistry for centralized screen configuration

* fix(#310): fix ktlint when-condition blank lines in Navigation.kt

* fix(#289): derive availableNavItems from ScreenRegistry

* fix(#310): use addEntryProvider instead of reified entry for dynamic screen registration

* fix(#310): remove double top padding from kanban, channels, achievements screens

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 18:19:38 +03:00
M57andM57 c1af989220 feat(#349): Smart auth login flow — probe dashboard and show correct fields (#351)
* feat(#349): smart auth login flow — probe dashboard, show correct fields

Landing screen with two buttons (Auth Login / Pairing Login) replaces
ConnectScreen as the start screen. Auth login probes the dashboard to
detect what auth it needs (token-only, basic-auth, or both) and shows
only the relevant input fields.

Changes:
- NavigationKeys.kt: add LandingScreen, AuthLoginScreen NavKeys
- LandingScreen.kt: new screen with hero icon + two login buttons
- AuthLoginScreen.kt: IP/Port input, probe logic, dynamic field rendering
- AuthLoginViewModel.kt: probe API, dashboard auth detection, connect flow
- Navigation.kt: wire new screens, start on LandingScreen, hide bottom bar
- strings.xml: add string resources for both screens

* fix(#351): align AuthLoginScreen field names with ViewModel API, fix TAG refs

- state.isProbing → state.probing
- DashboardAuthMode.BOTH → DashboardAuthMode.ALL
- viewModel.probe() → viewModel.probeDashboard()
- state.isConnecting → state.isLoading
- Removed unused TAG constant, replaced with inline tag string
- Made connectTokenOnly suspend to fix safeApiCall compilation

This fixes the CI compilation errors.

* fix(#351): revert probeDashboard() call to probe() to match VM

* fix(#351): rename isProbing to probing in ViewModel

* fix(#351): follow redirects in basic auth login — / redirects to /login

* fix(#351): show all fields when basic auth detected — still needs session token for WS

* refactor(#349): remove old ConnectScreen, clean up NavKey + navigation

* fix(#351): validate token against /api/sessions (auth-protected) instead of /api/status (public)

/api/status is a public endpoint that returns 200 even with wrong tokens,
causing invalid tokens to be saved and the WebSocket to spin forever on
the chat screen. /api/sessions requires auth and returns 401 on bad tokens.

* fix(#351): rewrite basic auth login — POST to /auth/password-login, then fetch SPA with cookie to extract token

* fix(#351): use ticket-based WS auth when basic auth is active

The updated dashboard on 0.0.0.0 always has auth_required=True, which
rejects legacy ?token= in favor of ?ticket= (single-use, 30s TTL).
Added AuthManager.setWsAuthParam() to support both ?token= (loopback)
and ?ticket= (gated mode) in wsUrl(). connectBasicAuth now mints a
ticket via POST /api/auth/ws-ticket after login instead of extracting
the token from the SPA.

* fix(#351): use requirePrefs() instead of direct prefs reference in wsUrl()

* fix(#351): handle relaxed-mock empty string in wsUrl() authParam fallback

* feat(#349): auto-extract session token from SPA HTML during probe

The probe now extracts the __HERMES_SESSION_TOKEN__ value from the
dashboard SPA and auto-populates the token field in TOKEN_ONLY mode,
eliminating the need for manual token pasting.

- Introduced ProbeResult data class to carry authMode + extractedToken
- Regex extraction of __HERMES_SESSION_TOKEN__ from dashboard HTML
- Token field auto-filled on probe completion for TOKEN_ONLY mode

* fix(#349): return BASIC_AUTH from probe when basic auth detected

Bug: probe returned ALL instead of BASIC_AUTH for basic-auth-only
dashboards, causing token+credential fields to show and the connect
flow to fall through to connectTokenOnly() on basic auth failure,
producing a confusing 'Unauthorized (HTTP 401)' NetworkResult error.

Fix: probe now returns DashboardAuthMode.BASIC_AUTH when a redirect
to /login is detected. BASIC_AUTH in connect() calls
connectBasicAuth() directly with no fallback. ALL mode also no longer
falls through to connectTokenOnly().

* fix(#349): use session cookie for REST API in gated mode

The dashboard's gated mode authenticates REST API requests via
session cookies (hermes_session_at), not Bearer tokens. The app
was storing the WS ticket as the token and sending it as
Authorization: Bearer <ticket> for all REST calls, which the
server rejected with 401.

- Extract hermes_session_at cookie from Set-Cookie header after
  POST /auth/password-login and store it in AuthManager
- ApiClient authInterceptor now sends Cookie header when session
  cookie is present, falls back to Bearer token for loopback mode
- ConnectResult carries both WS credential (ticket) and session
  cookie separately
- WS ticket used only for WebSocket (?ticket=), session cookie
  used for REST API

* feat(#349): add logout button to Settings > About

Red error-style button that clears all auth credentials (token,
session cookie, WS auth param) and navigates back to LandingScreen.
Wired through SettingsScreen onLogout callback to Navigation.kt.

* fix(#349): improve logout navigation flow

* fix(#349): keep backstack stable across logout

Use remember without keys for NavBackStack so the same backstack
object persists across startScreen changes. Use LaunchedEffect keyed
on startScreen to reset stack content atomically. This avoids
NavDisplay losing snapshot observation when a new NavBackStack
object is created.

* fix(#349): mint fresh WS ticket on every connect

The WS ticket is a single-use 30-second credential. On app restart
(or reconnect), the stored ticket is expired. Now refreshWsTicketIfNeeded()
mints a new ticket using the session cookie before every WS connection
attempt. Loopback mode (no session cookie) is unaffected.

* fix(#349): refresh ticket in openSocket for reconnect paths

scheduleReconnect() bypasses connect() and calls openSocket()
directly. Moving the ticket refresh into openSocket() ensures
every WS connection attempt — first launch and reconnect —
uses a fresh ticket.

* fix(#349): stub getSessionCookie() in ApiClientTest

mockkObject(AuthManager) is strict, so unstubbed getSessionCookie()
calls throw MockKException which manifests as IOException.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 16:37:09 +03:00
M57andM57 11ca9b55c8 feat: Reorganize settings into categorized tab pages (#345)
* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs

* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs
- Adheres to ktlint conventions

* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs
- Fixed UI bug by removing padding (`edgePadding = 0.dp`) in the TabRow to avoid excessive white space
- Adheres to ktlint conventions

* fix(#345): remove double padding on settings tabs

HermesScaffold already applies Scaffold's inner padding (top bar height +
status bar insets) via its own Box wrapper. The tabbed settings layout
was applying the same paddingValues a second time on the outer Column,
causing the tab row to sit ~64dp too low. Fix removes the redundant
.padding(paddingValues) call.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 11:26:10 +03:00
M57andM57 60bf6e8a33 feat: UI Changes
* fix(#220): implement edit soul and set model dialogs

* fix(#211): make UI more compact and information dense

* fix(#211): make navigation drawer sidebar more compact

* feat(#252): move settings item to end of navigation drawer

* feat(#250): add bottom navigation bar display mode settings

* feat(#225): add search bar filtering to remaining screens

* fix: make bottom navigation bar display modes more compact

* fix: resolve compilation error in KanbanScreen by referencing assignedTo

* feat: add search to achievements screen and top padding for icon-only navigation bar

* fix: adjust icon-only bottom bar height and remove padding

* fix(#249): remove unused PaddingValues imports in 6 screens

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-21 20:42:23 +03:00
Hy4ri 8e8fca756a feat(#208): add Monochrome, Gruvbox, Catppuccin and AMOLED theme presets 2026-06-21 13:01:55 +03:00
Hy4ri 78da6af6be feat(#232): dynamic light/dark theme switch without restart 2026-06-21 12:33:57 +03:00
Hy4ri cdad31b555 refactor(#78): extract UI strings for chat, settings, connect, kanban, and skills screens 2026-06-21 11:38:33 +03:00
Hy4ri 19d44c077b chore: format codebase with ktlint 2026-06-21 10:57:18 +03:00
M57 00c6e75f50 feat(connect): saved connection profiles support (#107) (#191)
* feat(#107): implement saved connection profiles support

* fix(connect): fallback profiles resolution and stubbing in tests
2026-06-21 00:18:35 +03:00
M57 64a047f095 feat: move session history to a dedicated history screen (#175)
* feat: move session history to a dedicated history screen

Closes #159.

* fix(ci): fix ktlint style issues and unresolved NavigationController reference
2026-06-20 22:27:54 +03:00
M57andM57 3c5fcf504c feat(#154): add word-by-word typing effect toggle and speed slider in settings (#165)
* feat(#154): add word-by-word typing effect toggle and speed slider in settings

* fix(settings): import Slider to fix build failure

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 17:09:42 +03:00
M57andM57 a2d299e136 feat(#129): remove redundant source archive, add About version card in Settings (#149)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 10:30:18 +03:00
M57 46b24593f9 feat(#58): implement skill markdown/yaml editor UI (#121) 2026-06-19 23:58:34 +03:00
Hy4ri 8755338636 Revert "Merge pull request #42 from Hy4ri/feat/sessions-screen"
This reverts commit 7d50b5acd7, reversing
changes made to ee97be50cb.
2026-06-19 23:18:26 +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
Hy4ri 508aea8bfa fix(#79): centralize retrofit error handling with NetworkResult 2026-06-19 22:58:33 +03:00
Hy4ri 2fce6ad97a feat(settings): add up/down arrow buttons to reorder bottom nav items 2026-06-19 18:41:02 +03:00
ronia 4327389b32 feat: dedicated Sessions browser screen (Issue #40) 2026-06-19 16:48:16 +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 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
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
ronia f833005bec fix(settings): route topbar Settings through NavigationController + Clear Token button
B7 (Jun 18 2026): user reported Settings page unresponsive when
reached via the ChatScreen topbar gear icon, but worked fine via
the drawer sidebar. Two fixes:

1. Route topbar gear through NavigationController.navigateTo()
   instead of raw backStack.add(). The NavigationController path
   (used by the drawer) deduplicates against the current top
   entry and matches the drawer's navigation flow exactly. The
   bypass path could stack duplicate SettingsScreen entries on
   rapid taps, which likely caused the unresponsive symptom.

2. Add 'Clear Token' OutlinedButton below the Auth Token field in
   Settings. One-tap wipes a corrupted / rejected token and saves
   immediately, without having to select-and-delete the masked
   password field character by character. Recovers from the
   'I gave you a bad token' scenario in seconds.
2026-06-18 03:56:04 +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
Hy4ri 3ff148f6d0 Fix CronJob model API compatibility and register BackHandler in SettingsScreen 2026-06-16 00:21:31 +03:00
Hy4ri d6ff3de594 Initial commit of HermesControl Android app 2026-06-16 00:00:36 +03:00