Commit Graph
47 Commits
Author SHA1 Message Date
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
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
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
Hy4ri bd439ba674 style: modernize UI aesthetics and replace app launcher icons 2026-06-17 00:42:14 +03:00
Hy4ri d4328d0fb2 feat: add pairing / connection string parsing & verify with unit tests 2026-06-16 23:13:04 +03:00
Hy4ri bfa959ed05 test: add ModelViewModel success path test 2026-06-16 18:17:56 +03:00
Hy4ri 3830236585 feat: slash command autocomplete, dynamic theme colors, interactive model selection & drawer icons 2026-06-16 18:15:01 +03:00
Hy4ri c06e8417db ci: opt-in to Node 24 for actions to avoid deprecation warnings 2026-06-16 17:51:59 +03:00
Hy4ri f355814842 fix: plugin status toggling & fix tests compilation 2026-06-16 17:38:12 +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
Hy4ri 8ca4599c6a fixes 2026-06-16 00:52:36 +03:00
Hy4ri 0562906634 fix: replace refresh text button with refresh icon across all screens (Kanban, Cron Jobs, Skills, Channels, Plugins, System) 2026-06-16 00:41:27 +03:00
Hy4ri 0b1822c6a5 ci: integrate gradle action, android static analysis linting, and reports uploading 2026-06-16 00:39:29 +03:00
Hy4ri 46f58de5dc ci: add GitHub Actions workflow to run lint, test, and build APK 2026-06-16 00:38:43 +03:00
Hy4ri edc2d45966 fix: correct log response key to 'lines' and convert refresh text to icon button in LogsScreen 2026-06-16 00:36:58 +03:00
Hy4ri 69f36b1606 fix: resolve double-padding on chat screen when keyboard is opened 2026-06-16 00:34:45 +03:00
Hy4ri 48bf0a1ffc feat: add full chat slash commands support (/help, /status, /sessions, /stats) 2026-06-16 00:32:10 +03:00
Hy4ri 21a020c87e feat: load message history when switching or resuming a session 2026-06-16 00:27:00 +03:00
Hy4ri c1d0956a1d feat: add button to create new chat directly in ChatScreen top bar actions 2026-06-16 00:23:57 +03:00
Hy4ri 3ff148f6d0 Fix CronJob model API compatibility and register BackHandler in SettingsScreen 2026-06-16 00:21:31 +03:00
Hy4ri 9b2aeae836 Add vertical scrolling modifier to sidebar drawer navigation 2026-06-16 00:16:33 +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 cffb68550e Update progress file for MCP, Webhooks, Models screens 2026-06-16 00:07:41 +03:00
Hy4ri 43dfbc5470 Implement MCP Servers, Webhooks, and Models screens 2026-06-16 00:06:43 +03:00
Hy4ri ce71aa3e68 Update PROGRESS_STATUS.md with Pairing and Config status 2026-06-16 00:02:50 +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