Commit Graph
26 Commits
Author SHA1 Message Date
M57 dd5ce10796 feat(kanban): add rich task card, filter sheet, and task detail screen 2026-09-14 22:58:48 +03:00
M57 69cd34d289 feat(vault): credential vault status and mid-turn auth prompt handling (#1090) (#1097) 2026-09-13 00:20:07 +03:00
M57andM57 f04d878358 feat(bots): add multi-agent group chat collaboration room (#998)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-28 17:50:37 +03:00
M57 48c9039e02 feat(ui): add BotsScreen and BotsViewModel with active presence strip (#977) 2026-08-27 01:49:31 +03:00
M57andM57 063c27b141 feat(settings): move language to its own section with scalable dropdown picker (#906)
- New Language row in the main Settings list with current-language summary
- New SettingsLanguage drill-down page with an OutlinedButton + DropdownMenu
  picker (scales to any number of languages; same pattern as theme presets)
- Shared supportedLanguages()/languageLabel() helpers — adding a language
  is now one entry in one place
- Removed the 4-option segmented pill row from the Appearance page
- Added settings_sec_language string (en + zh)

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 12:18:37 +03:00
M57andM57 7d00d7b81b feat(memory): dedicated Memory screen (moved out of System tab) (#835)
* feat(memory): dedicated Memory screen — providers, builtin files & reset (moved from System tab)

* feat(memory): move self-improvement & learning activity section from System tab to Memory screen

* fix(memory): remove top padding, single-line reset buttons, providers spacing

* fix(memory): tighten provider card padding

* fix(memory): distinct AutoStories icon for Memory tab

* chore: ignore local debug apk

* feat(ui): fresh icon set for drawer screens

* feat(ui): custom Neurology icon for Memory tab

* fix(ui): theme-driven fill for NeurologyIcon (checkColorLiterals)

The custom Neurology vector used SolidColor(Color.Black), tripping the
hardcoded-color guard (issue #622). The fill only contributes alpha —
Icon()'s tint replaces the RGB at draw time, mirroring Material's own
icon set — so the literal belongs in theme/ like every other hardcoded
color: new IconDefaultFill token, scoped there by the guard's design.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-07 04:18:50 +03:00
M57andM57 fd5d851405 feat(plugins): memory provider config & setup management (issue #783) (#834)
* feat(plugins): memory provider config & setup management (issue #783)

* fix(memory): remove top padding on provider detail screen

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-07 04:18:50 +03:00
M57andM57 fe79ff42b4 feat(toolsets): per-toolset config/env/provider/post-setup management (#782) (#831)
* feat(toolsets): per-toolset config/env/provider/post-setup management (#782)

* fix(toolsets): per-entry toolset name for shared detail ViewModel (#782)

NavDisplay uses the activity-level ViewModelStoreOwner, so class-keyed
viewModel{} returns the same ToolsetDetailViewModel for every detail
entry; the constructor-captured name froze the first-opened toolset into
all entries (on-device: every toolset showed web's providers, no further
/config calls after the first). The screen now drives setToolset(name) +
loadConfig() per entry, and setToolset resets per-toolset state
(config, revealed secrets, running post-setup) so nothing bleeds across
toolsets.

* style(toolsets): single-line scrollable tool chip strip (#782)

A wrapping FlowRow stacked chips vertically for toolsets with many
tools (web etc.), ballooning card height — especially on large font
scales. Chips now sit on one horizontally scrollable row.

* chore(toolsets): drop stray terminal-noise line from chip strip change

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-06 17:57:58 +03:00
M57andM57 5870b73397 feat(#776): pairing management screen (#791)
REST + Channels-style UI for DM pairing (list pending/approved, approve, revoke, clear).

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-03 19:49:31 +03:00
M57andM57 6aee02c4da feat: add Files tab backed by the managed-files endpoint (#727)
* feat: add Files tab backed by the managed-files endpoint

Add a Hermes Mobile tab that browses the gateway's managed-files root via the
existing /api/files* backend routes: list/navigate folders, breadcrumb path,
create folder, upload (streaming multipart), delete, and open files via the
authenticated GatewayFileClient download URL.

- data/model/ManagedFile: entry + list/read/upload/mkdir/delete models
- HermesApiService: list/read/upload(+stream)/mkdir/delete endpoints
- FilesViewModel: state, navigation, per-row busy, transient-state cleanup
- FilesScreen: folder list, breadcrumb, FAB new folder, upload picker,
  delete confirm, open via system viewer
- NavigationKeys + ScreenRegistry + strings: register in Inspect drawer section

* fix(files): open files in-app via FileProvider instead of browser URL

The previous file-open path handed a ?token=-authenticated URL to the system
browser, but /api/files/download only accepts the ephemeral in-process session
token as ?token=, not the mobile's Bearer token — so the request failed and the
browser rendered the JSON error body instead of the file.

Fix: download the file in-app through the authenticated client (Bearer/
session-cookie, same path as the rest of the app), decode the base64 data_url,
write it to cacheDir, and open it via FileProvider.getUriForFile + ACTION_VIEW
(mirrors the existing ChatScreen/SystemViewModel open-from-cache pattern).
Works on a remote phone since the file is materialized locally first.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-26 22:46:36 +03:00
M57andM57 0edb67338d refactor: remove bottom navigation bar (#665 #666 #667 #668) (#672)
* refactor(nav): remove bottom NavigationBar from Scaffold (#665)

* refactor(theme): remove BottomNavDisplayMode enum (#666)

* refactor(settings): remove nav bar customization page (#667)

* chore: clean up unused imports after bottom nav removal (#668)

* chore: remove dead bottom nav infrastructure after removal

- Remove SettingsNavBar NavKey (NavigationKeys.kt)
- Remove bottomNavItems field from ServerStoreState + migration code
- Remove _bottomNavItemsFlow, getBottomNavItems, setBottomNavItems from AuthManager
- Remove bottomNavItems handling from selfHealed() (ServerCollectionOps)
- Remove redundant bottomBar={} from Scaffold (Navigation.kt)
- Remove unused nav bar strings from values/strings.xml + values-ko/strings.xml
- Remove content_desc_move_up/down/remove_item strings
- Update SettingsViewModelTest.kt (drop stale getBottomNavItems mock)
- Update ServerStoreTest.kt (remove bottomNavItems assertions)

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-22 10:14:50 +03:00
M57andM57 a62bc7e16c refactor: remove device Pairing feature entirely (issue #645) (#650)
* refactor: remove device Pairing feature entirely (issue #645)

- Delete Pairing.kt model + pairing API endpoints (get/approve/revoke/clear)
- Delete PairingScreen/PairingViewModel + PairingCodeEntryScreen/ViewModel
- Remove NavKeys, ScreenRegistry drawer entry, Navigation.kt entry + Landing pairing button
- Remove pairing string resources (en + ko); keep connect_* config-parse strings
- Keep Telegram onboarding pairing_id flow (separate concept, per issue)
- Fix related doc comments + remaining test reference

* test: fix racy CONNECTED read in gated-mode WS test (issue #635 test)

testGatedMode_attemptsTicketFetchWithoutBareCookie asserted CONNECTED
immediately after the server-side onOpen latch fired, racing the client
handshake (WsListenerImpl sets CONNECTED on receipt of the 101). Await the
real status transition via connectionStatus.first{==CONNECTED} like the
other connect tests do. No product behavior changed.

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 23:23:29 +03:00
M57andM57 00c0ddfb92 feat(billing): adopt subscription.* + usage.bars WS methods (issue #628) (#629)
* feat(billing): adopt subscription.* + usage.bars WS methods (issue #628)

Add Billing/Plan screen + WS client surface for the backend release audit
(hermes-agent 0bf44d557..614dc194e). credits.view was removed upstream;
subscription.{state,preview,change,resume,upgrade} + usage.bars replace it.

- WsMethods: add SUBSCRIPTION_* + USAGE_BARS constants
- BillingModels: @Serializable request/response (snake_case, ignoreUnknownKeys)
- BillingRepository: WS dispatch via HermesWsClient.request + typed decode;
  HermesRpcException (-32601) surfaces as featureUnavailable, not a crash
- BillingViewModel: plan/usage load + upgrade/change/resume/cancel actions
- BillingScreen: HermesScaffold composable (plan card, usage bars, actions)
- NavigationKeys + ScreenRegistry + strings (en/ko) wire BillingScreen into
  the INSPECT drawer section

Verified: ./gradlew ktlintCheck + compileDebugKotlin both BUILD SUCCESSFUL.

* fix(billing): align models + UI to live gateway wire shape (issue #628)

Live-probed the 6 subscription.* / usage.bars RPCs against the running
backend (hermes-agent dashboard :9119, 2026-07-19). Findings:

- subscription.state returns logged_in / current / tiers / usage (nested),
  NOT the legacy subscription_type_id / renews_at fields the models assumed.
- Mutating RPCs (preview/change/resume/upgrade) return ok:false *results*
  with error/message/payload (e.g. invalid_token when not portal-authed),
  not RPC-level errors — so the ViewModel's HermesRpcException guard never
  fired and the screen silently half-decoded null fields.
- subscription.upgrade also returns an idempotency_key not in the model.

Changes:
- BillingModels: re-shape SubscriptionStateResponse around current/tiers/
  usage; add ok/error/message/payload envelope + idempotency_key to the
  action responses; add SubscriptionCurrent / SubscriptionTier / preview
  payload types. All nullable + ignoreUnknownKeys (graceful degrade).
- BillingViewModel: detect ok:false results on every call and route to
  featureUnavailable / errorMessage instead of swallowing them.
- BillingScreen: read plan from subscription.current; render a clean
  'no active plan' / 'log in to Nous Portal' card on the logged-out path
  instead of a broken null plan card.
- strings: add billing_no_active_plan / billing_login_required (en + ko).

Verified: ./gradlew ktlintCheck + compileDebugKotlin both BUILD SUCCESSFUL.

* fix(billing): address independent review (agy) — error routing + icon

Triage of the independent AGY review on PR #629:

Fixed (real issues):
- BillingViewModel: usage.bars RPC failure (-32601) no longer marks the
  whole feature unavailable (was blocking the plan card despite the
  'non-fatal' comment); it is now correctly non-fatal.
- BillingViewModel: a failed primary subscription load now surfaces
  errorMessage even when usage bars loaded (was swallowed by
  'sub == null && bars == null').
- BillingScreen: render state.errorMessage as an error card inside
  BillingContent so failed mutating actions (upgrade/cancel) are visible
  instead of silently disappearing.
- BillingScreen: wrap the dead AccountBalanceWallet expression in a real
  Icon() composable in FeatureUnavailableState.

Declined (with rationale, recorded for maintainer):
- Tier-select / upgrade UI (review issue #4): explicitly out of scope per
  the PR body — 'Plan-selection UI ... is not built ... left for a
  follow-up once backend returns the available plans.'
- Rename snake_case Kotlin props to camelCase (review nit #2): incorrect —
  OkHttpProvider uses JsonNamingStrategy.SnakeCase, which maps snake_case
  wire keys onto snake_case properties; the props are correct. Fixed the
  misleading doc comment instead.

* fix(billing): match REAL logged-in wire shapes + kill null-result crash

The live gateway probe (logged-in Nous Portal session, 2026-07-19) showed
the earlier models assumed the WRONG shape:

- subscription.state.current is {tier_id, tier_name, monthly_credits,
  credits_remaining, cycle_ends_at, pending_downgrade_*} — NOT the legacy
  subscription_type_id/status/renews_at fields. Screen rendered 'Free / no
  plan' for a real logged-in user.
- usage.bars is {available, status, plan_name, plan_bar{remaining_display,
  total_display, pct_used, fill_fraction}, topup_bar} — NOT a List<UsageBar>.
- Mutating RPCs return error:'insufficient_scope' when the Portal token
  lacks billing:manage scope (ok:false result, not an RPC error).

Crash fix (root cause of 'Billing RPC returned no result payload'):
- BillingRepository.decode() now returns null on a missing/null result
  instead of requireNotNull() -> IllegalStateException. All repo methods
  return nullable; ViewModel handles null as a non-fatal 'No billing data
  returned' instead of crashing.
- BillingScreen.PlanCard / UsageBarRow rewritten to the real fields; the
  resume/cancel branch now gates on can_change_plan (no status field exists
  on current anymore).
- UsageBarsResponse + UsageBar reshaped to plan_bar/topup_bar; added
  billing_credits_remaining / billing_plan / billing_topup strings (en+ko).

Verified: ktlint clean + compileDebugKotlin BUILD SUCCESSFUL. Decode sim
against the captured live responses confirms PlanCard/UsageBarRow/error
paths all resolve with no null-crash.

* fix(billing): decode WS result that arrives as toAny() Map, not JsonElement

Root cause of 'No billing data returned' on a logged-in server: the backend
returns a valid subscription.state result, but HermesWsClient.request()
yields result already converted via JsonElement.toAny() (a Map<String,Any?>),
not a raw JsonElement. BillingRepository.decode() only accepted JsonElement,
so  was null and the repo returned null -> ViewModel showed
the generic 'No billing data returned' message. The server DOES have billing
(logged_in:true, tier_name:Free, /usr/bin/bash.10 credits) — the app just couldn't parse it.

Fix: decode() now normalizes whatever arrives — a kotlinx JsonElement OR the
toAny() Map/List — back into a JsonElement via a recursive Any->JsonElement
walk, then deserializes. Both shapes decode correctly now.

This also means the previous 'Billing RPC returned no result payload' crash
and the generic 'no data' message were both symptoms of this same parse gap,
not a missing backend feature.

Verified: ktlint clean + compileDebugKotlin BUILD SUCCESSFUL.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-19 15:06:09 +03:00
M57andhy4ri 3e357594be refactor(settings): drill-down navigation replacing tabbed layout (#605)
* refactor(settings): drill-down navigation replacing tabbed layout

Replaces the 4-tab settings layout with a native Android-style drill-down:
a single category list (SettingsScreen) where each row pushes a dedicated
sub-page onto the Navigation3 back stack (SettingsConnection / Appearance /
Chat / NavBar / Behavior / About). Each sub-page is its own NavKey destination
registered in Navigation.kt and reuses the existing section composables.

- SettingsScreen becomes the category list with icon + label + summary
  (Appearance row shows live theme + language, e.g. "Dark · 한국어").
- New SettingsSubPages.kt hosts the 6 full-screen sub-pages.
- AboutSection promoted out of SettingsScreen into its own sub-page.
- SettingsViewModel remains the single source of truth; no persistence changes.
- Adds settings_summary_* strings for list subtitles.

Consumes the in-app language switcher from the prior commit (AppearancePage
passes appLanguage through to AppearanceSection).

* [bug] fix settings sub-page back button opening drawer on first tap

The ModalNavigationDrawer wraps the whole app and stays gesture-enabled on
the Settings root (a DRAWER_GESTURE_SCREEN). Drilling into a sub-page
(SettingsConnection/Appearance/Chat/NavBar/Behavior/About) disables gestures,
but the existing LaunchedEffect only snapped the drawer closed when it was
already isOpen. The drawer's scrim surface lingered during the transition and
intercepted the first back-button tap, sliding the sidebar open instead of
navigating back.

Now snapTo(Closed) unconditionally whenever gestures are disabled — a safe
no-op when already closed — so the sub-page back button works on the first tap.

* [bug] fix settings sub-page back button opening drawer (gesture-surface race)

The first "back" tap on a settings sub-page opened the sidebar instead of
navigating back. Root cause: Settings root is a gesture-enabled drawer screen,
and the sub-pages are the first destinations ever drilled down from it. When
navigating in, ModalNavigationDrawer's gesture interceptor surface lingered
for a frame (gesture -> non-gesture transition), so the top-left tap was
consumed by the drawer instead of the Back IconButton. The previous snapTo
fix only reset drawer *state*, not the gesture layer, so it didn't help.

Fix: NavigationController now holds a closeDrawer hook (set by MainNavigation)
and invokes it synchronously inside navigateTo() when pushing a non-primary
sub-page. The drawer is dismissed in the same tap handler that triggers the
navigation, before any recomposition, so the sub-page is never tappable while
the gesture surface is still mounted. The LaunchedEffect(gesturesEnabled)
snapTo remains as a backstop for non-navigateTo entries.

* fix(#603): delay drawer gesture activation to prevent residual swipe

* fix(#603): fix settings back navigation and drawer auto-open

---------

Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
2026-07-16 01:23:19 +03:00
M57andM57 9e10c813d4 feat(#534): add OAuth provider management screen + credential_warning surfacing (#566)
* feat(#534): add OAuth provider management screen + credential_warning surfacing

* style(#534): fix ktlint import-ordering in ProvidersViewModel

* fix(#534): address agy review — terminal DONE/ERROR dialog states + PKCE browser launch

* fix(#534): add Done/Close dialog strings

* fix(#534): address agy review - terminal DONE/ERROR dialog states, real PKCE browser launch, clear flow state

---------

Co-authored-by: M57 <m57@hermes-control.app>
2026-07-12 21:10:11 +03:00
M57andM57 9156064d2b feat(#537): analytics screen — usage + per-model stats (#558)
* feat(#537): analytics screen — usage + per-model stats from /api/analytics/*

* fix(#537): address PR review — cancel-in-flight loads, retain cache on refresh, drop redundant async

* fix(#537): surface real analytics failure, mirror backend shape (period_days/tools), round-trip test

* fix(#537): tolerate float last_used_at in analytics (backend emits epoch as number)

* fix(#537): type tools[] as AnalyticsToolUsage (tool/count/percentage), render Top tools section

* fix(analytics): fix crashes and refactor tab layout

* perf(analytics): cache responses and prefetch day windows

---------

Co-authored-by: M57 <m57@hermes-control.app>
2026-07-12 00:04:56 +03:00
M57andM57 afa82ceec2 feat(#532): add process.list + process.kill manager screen (#556)
Co-authored-by: M57 <m57@hermes-control.app>
2026-07-11 20:17:03 +03:00
M57andM57 7811a35d29 feat(#350): pairing login flow with QR scan + manual entry (#421)
* feat(#350): pairing login flow with QR scan + manual entry

Adds PairingCodeEntryScreen with CameraX + ML Kit QR scanning
and manual code entry. The screen parses hermes://connect URIs,
Base64-encoded JSON, and raw token formats, validates via
/api/status, and saves credentials on success.

- New PairingCodeEntryScreen + PairingCodeEntryViewModel
- CameraX 1.4.1 + ML Kit barcode-scanning 17.3.0
- CAMERA permission + proguard rules
- Landing screen Pairing Login button wired correctly

* fix(#350): resolve smart-cast on delegated property for errorMessage

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>

* refactor(#421): remove QR scanner from pairing login

- Remove CameraX + ML Kit barcode dependencies
- Drop CAMERA permission and camera feature from manifest
- Strip QrScannerTab, QrCodeScannerView, processFrame composables
- Remove camera permission state/methods from ViewModel
- Purge all QR/camera strings from strings.xml
- Delete ML Kit proguard rules

The pairing login now shows only the manual code entry UI.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-30 10:08:01 +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
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
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
ronia 4327389b32 feat: dedicated Sessions browser screen (Issue #40) 2026-06-19 16:48:16 +03:00
Hy4ri 03916014c0 Implement Logs, Plugins, Channels, Keys, System, and Kanban screens with full integration tests and code formatting checks 2026-06-16 00:13:18 +03:00
Hy4ri 43dfbc5470 Implement MCP Servers, Webhooks, and Models screens 2026-06-16 00:06:43 +03:00
Hy4ri a8cf79d91c Implement Device Pairing and Config screens 2026-06-16 00:02:05 +03:00
Hy4ri d6ff3de594 Initial commit of HermesControl Android app 2026-06-16 00:00:36 +03:00