* Fix: Prevent AuthManager state pollution on connection verification
Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.
Fixes#260
* Fix: Prevent AuthManager state pollution on connection verification
Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.
Fixes#260
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Issue #278 reported that `AuthManager.getToken()` (which reads from
`EncryptedSharedPreferences`) was being called synchronously inside the
`MainNavigation` composable, causing UI thread blocks during navigation
transitions (PERF-10).
This commit resolves the issue by caching the token in a new `tokenFlow`
(`MutableStateFlow`) within `AuthManager`.
- The flow is initialised with the token during `AuthManager.init()`.
- Updates to the token (`setToken`, `setSelectedProfileId`,
`setProfileToken`) are synchronised with `tokenFlow`.
- `MainNavigation` now correctly observes this state using
`collectAsState()`, preventing disk-read blocking on recompositions.
Impact: Navigation transitions are now fully non-blocking. No side effects
are expected as state synchronisation maintains parity with persistent storage.
Resolves#278
* 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>
Extract the overly long `entryProvider` lambda out of `MainNavigation` into a separate `appEntryProvider` function to improve code readability and maintainability.
* ⚡ Optimize collection creation in Navigation.kt
Replaced uses of `.map { ... }.toSet()` with `.mapTo(mutableSetOf()) { ... }` in Navigation.kt. This avoids the creation of intermediate ArrayList objects, saving unnecessary memory allocations.
* ⚡ Optimize collection creation in Navigation.kt
Replaced uses of `.map { ... }.toSet()` with `.mapTo(mutableSetOf()) { ... }` in Navigation.kt. This avoids the creation of intermediate ArrayList objects, saving unnecessary memory allocations. Fixed ktlint formatting issues causing CI failure.
Replaces `.map { ... }.toSet()` with `.mapTo(mutableSetOf()) { ... }`
when resolving `bottomNavKeys` to avoid creating and discarding an
intermediate `ArrayList` during recomposition.
* feat: move session history to a dedicated history screen
Closes#159.
* fix(ci): fix ktlint style issues and unresolved NavigationController reference
* 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>
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.
## 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
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.
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.
* 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>