Commit Graph
33 Commits
Author SHA1 Message Date
M57andHy4ri 9661246497 audit(#622): sweep hardcoded colors + add checkColorLiterals guard (#626)
Removed docs/color-hardcoded-audit.md per request. Replaced 2 hardcoded
Color literals with theme tokens; added CI guard blocking new literals.

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
2026-07-18 16:15:45 +03:00
Hermes Agent 2043ba499e feat: publish version.txt alongside APK for F-Droid version extraction 2026-06-28 11:13:56 +03:00
M57andM57 40b08ec4e8 feat: rename app from 'Hermes Control' to 'Hermes Mobile' (#412)
* feat: rename app from 'Hermes Control' to 'Hermes Mobile'

Updates the display name in launcher icon, About screen, and
landing/auth screen. Package name (com.m57.hermescontrol) unchanged.

* chore: update release workflow references to Hermes Mobile

Renames keystore file, APK artifact, and GitHub Release name
from 'hermes-control' / 'Hermes Control' to 'hermes-mobile'
/ 'Hermes Mobile'.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-27 21:39:39 +03:00
M57 e3d6be5725 chore: bump actions/github-script from 7 to 9
chore: bump actions/github-script from 7 to 9
2026-06-25 22:59:25 +03:00
dependabot[bot] ad1a2a59c9 chore: bump actions/cache from 4 to 6
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 19:49:03 +00:00
dependabot[bot] fc3e36ed0f chore: bump actions/github-script from 7 to 9
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 19:48:58 +00:00
M57andM57 a596fa4383 chore(#376): upgrade build JDK from 17 to 21 (#377)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-25 22:36:27 +03:00
M57andM57 cc40658661 feat: add merge conflict detector workflow (#352)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 18:52:22 +03:00
M57andM57 42fcf2a14b fix: implement in-memory token caching to avoid redundant prefs decryptions (#320)
* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.
- Fixed merge conflicts and ensured code still tests well.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 22:19:35 +03:00
M57andM57 eb71ae3a46 Fix WebSocket events being silently dropped on buffer overflow (#313)
* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix ktlint formatting issues in HermesWsClient.kt

The formatting changes adhere to ktlint rules required by the CI pipeline:
- Removed a trailing comma in MutableSharedFlow initialization to avoid multiline expression wrapping errors.
- Sorted imports to be in lexicographical order.

These formatting changes fix the failing CI check.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 22:13:55 +03:00
M57andM57 2c6e437a5b Fix: Prevent AuthManager state pollution on connection verification (#348)
* 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>
2026-06-23 20:49:58 +03:00
96b9a14e28 Add workflow to label PRs with merge conflicts (#342)
* 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)

* Add GitHub workflow to check PR for linked issue

* Add workflow to label PRs with merge conflicts

* Remove pr-issue-check.yml, keep only merge-conflict-label workflow

* Remove issue-tag.yml, keep only merge-conflict-label workflow in PR

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes@m57.local>
Co-authored-by: Hermes Agent <hermes@agent.local>
2026-06-23 12:36:57 +03:00
M57andM57 efba360d8a fix(ci): add release-compile safeguard to catch debugImplementation regressions (#336)
- Reverts okhttp.logging dependency from debugImplementation back to
  implementation so it's on the release classpath
- Adds compileReleaseKotlin CI job to catch release-variant compilation
  errors on every PR
- Fixes signing config to allow compileReleaseKotlin without keystore env
  vars (graceful fallback instead of hard require())
- Updates AGENTS.md with dependency scoping pitfall and CI docs

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 05:46:11 +03:00
M57andM57 f870f96fe0 fix(#294): document KSP standalone versioning, harden ci-summary reporting
- A2: Add inline comment to libs.versions.toml explaining ksp version
  is standalone per KSP 2.0+ guidance (not kotlinVersion-kspVersion)
- A3: Rewrite ci-summary check job to report each dependency's result
  individually with explicit skipped/cancelled/failure handling

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-22 15:11:14 +03:00
M57andM57 15a4748634 test(#292): coverage gaps TEST-04 through TEST-14 (#302)
* test(#292): coverage gaps TEST-04 through TEST-14

- TEST-04: ApiClient auth interceptor + BuildConfig.DEBUG gate (6 tests)
- TEST-05: SettingsViewModel profile operations (5 tests)
- TEST-06: ConnectViewModel profile switch edge cases (8 tests)
- TEST-07: EventParser untested subtypes (12 tests)
- TEST-08: AuthManager token routing through profiles (5 tests)
- TEST-09: ChatScreen instrumented compose tests (3 tests)
- TEST-10: WS reconnect state recovery (5 tests)
- TEST-11: Multi-profile token scenarios (6 tests)
- TEST-12: Chat DAO Room operations (11 tests)
- TEST-13: ChatViewModel slash command handling (8 tests)
- TEST-14: Predictive back gesture instrumented test (1 test)

Total: 70 tests across 11 files. CI updated with new
instrumented-tests job via android-emulator-runner.

* fix(ci): resolve ktlint, tests, and emulator failures

* fix(ci): resolve test failures on PR #302

* fix(ci): more ChatMessageDaoTest context mocks, re-read state after advanceUntilIdle in connect test

* fix(ci): remove problematic packageManager mock, decouple instrumented tests from CI gate

* fix(ci): switch emulator to default target, add more cores, memory, and timeout

* fix(ci): remove mockkStatic(Log::class) from ChatMessageDaoTest - fails on Android stubs

* fix(ci): use aosp_atd emulator image + KVM + AVD cache for fast instrumented tests

* fix(ci): remove ApplicationInfo mock - Android stub class breaks MockK

* fix(ci): rewrite ChatMessageDaoTest to use HermesDatabase.setForTest() pattern - Room in-memory DB requires Android SDK not available in JVM tests

* fix(ci): use actions/cache@v4 instead of @v7 (doesn't exist)

* fix(ci): add missing performClick import in StateViewsTest

* fix(ci): mock ChatViewModel in instrumented tests, add mockk to androidTestImplementation

* fix(ci): add asStateFlow import in ChatScreenTest

* fix(ci): exclude duplicate META-INF LICENSE/NOTICE files from androidTest build

JUnit Jupiter 5.8.2 JARs each ship META-INF/LICENSE.md,
causing mergeDebugAndroidTestJavaResource to fail with
'duplicate files found'. Add LICENSE.md, LICENSE, NOTICE,
and NOTICE.md to the packaging exclusion list.

* fix(#302): exclude duplicate META-INF/LICENSE-notice.md from instrumented test APK

JUnit Jupiter 5.x JARs all ship META-INF/LICENSE-notice.md,
causing mergeDebugAndroidTestJavaResource to fail.

* fix(#302): add LICENSE-notice.md as separate exclude entry

The Ant-style glob brace expansion in packaging.resources.excludes
doesn't handle LICENCE-notice.md inside the {}-enclosed pattern.
Adding it as a separate entry instead.

* fix(#302): use mockk-android for instrumented tests

io.mockk:mockk uses Java instrumentation agents not available on
Android. Switch to io.mockk:mockk-android for connectedDebugAndroidTest
which uses an Android-compatible mocking strategy.

* fix(#302): use createAndroidComposeRule for test lifecycle isolation

Standard createComposeRule has a gap where consecutive test methods
can lose the compose hierarchy on the emulator. Switching to
createAndroidComposeRule<MainActivity> gives explicit activity
lifecycle management across test methods, fixing the
'No compose hierarchies found' errors.

* fix(#302): use createComposeRule instead of createAndroidComposeRule<MainActivity>

MainActivity.onCreate() already calls setContent, so the test's
setContent { ChatScreen(...) } crashed with 'has already set content'.
Switch to createComposeRule() which launches a bare ComponentActivity
that doesn't call setContent, allowing the test to compose ChatScreen
in isolation with a mock ChatViewModel.

* fix(#302): use createAndroidComposeRule<ComponentActivity> + unmockkAll for instrumented tests

ChatScreenTest was failing with 'No compose hierarchies found' because
mockk mocks in instrumented tests weren't cleaned up between test runs.
Switch from createComposeRule() to explicit createAndroidComposeRule<ComponentActivity>()
and add @After tearDown() with unmockkAll() to ensure mock state doesn't
leak between test methods.

* fix(#302): wrap ChatScreen in HermesControlTheme in instrumented tests

ChatScreen accesses MaterialTheme.colorScheme during composition (background
gradient). Without a Material3 theme wrapper, the composable silently crashes
and no compose hierarchy is registered — causing 'No compose hierarchies found'
on any subsequent onNodeWithTag() call.

Fix: wrap ChatScreen in HermesControlTheme inside setContent {}, matching how
MainActivity renders the app. Also use createAndroidComposeRule<ComponentActivity>
instead of createComposeRule() for explicit lifecycle management, and add
unmockkAll() in @After to clean up mockk state between tests.

* fix(ci): fix packaging resource paths and use pickFirsts for duplicate META-INF files

Remove leading slashes from META-INF paths (AGP paths are
relative, not absolute). Use pickFirsts instead of excludes
to handle duplicate files from JUnit Jupiter JARs.

* fix(#302): use createAndroidComposeRule<ComponentActivity> for test isolation

createComposeRule() has a known race where the compose hierarchy
is lost between sequential test methods on the emulator.
createAndroidComposeRule<ComponentActivity> provides proper
Activity lifecycle management while using the same lightweight
empty activity that doesn't pre-set content (unlike MainActivity).

* fix(#302): consolidate ChatScreenTest into single test method

The Compose test rule's lifecycle gap between test methods on the
emulator causes 'No compose hierarchies found' on consecutive
setContent calls. Consolidating all three test scenarios into one
invocation eliminates the cross-test teardown issue entirely.

* fix(#302): mock ContextCompat to suppress permission dialog in test

The ChatScreen LaunchedEffect requests POST_NOTIFICATIONS via
ActivityResultContracts.RequestPermission(), which pauses the
activity and disposes the compose hierarchy. Mocking
ContextCompat.checkSelfPermission to return PERMISSION_GRANTED
prevents this from happening during the test.

* fix(#302): set connectionStatus=CONNECTED in ChatScreenTest

The default ChatUiState has connectionStatus=DISCONNECTED, which
makes isConnected=false, in turn disabling the chat input field
(enabled = isConnected) and the send button (canSend checks
isConnected). Set it to CONNECTED so both controls are active.

* fix(#302): add ConnectionStatus import and fix ktlint wrapping

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-22 13:49:04 +03:00
M57 63a927fae2 chore: Update softprops/action-gh-release to v3 to use Node 24 (#189)
Fixes the deprecation warning for Node 20 by bumping the action version.
2026-06-21 00:15:29 +03:00
Hy4ri a2d0aa0785 ci: upgrade all setup/cache/checkout actions to resolve deprecation warnings
Upgrade checkout@v7, setup-java@v5, cache@v5, upload-artifact@v7, and setup-gradle@v6 to support Node 24 natively.
2026-06-20 18:07:30 +03:00
Hy4ri 59be56260b ci: remove forcing actions to node24
Avoid deprecation warnings related to forcing Node.js 24 on Node.js 20 actions.
2026-06-20 18:01:53 +03:00
M57 82f2a5142f ci: update release workflow to Node.js 24 and setup-gradle validation (#173)
Closes #171.
2026-06-20 17:49:41 +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
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
ronia d6966afb7b ci(release): add missing mkdir -p release before source archive
The Create source archive step tried to write into release/ without
creating the directory first, causing fatal error on run #8.
2026-06-19 20:23:46 +03:00
ronia 4e6525689d feat: attach source code archive to release assets (closes #54) 2026-06-19 18:47:50 +03:00
Hy4ri 67c633396e ci: cache ktlint binary in GitHub Actions workflows 2026-06-19 17:57:22 +03:00
ronia 34e4c2aed4 fix: sign release APKs with stable keystore from secrets 2026-06-19 17:34:34 +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
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
Hy4ri c06e8417db ci: opt-in to Node 24 for actions to avoid deprecation warnings 2026-06-16 17:51:59 +03:00
M57 72e3a06047 ci: install ktlint before running ktlintCheck 2026-06-16 01:19:14 +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