feat(#433): achievements tab — full dashboard parity (#450)

* feat(#433): achievements 1:1 parity with dashboard

- Expanded model: AchievementsResponse now carries header stats
  (unlockedCount, discoveredCount, secretCount, totalCount, isStale,
  scanMeta/ScanStatus) and per-achievement fields (unlockedAt, secret, criteria)
- Added API endpoints: scan-status, rescan, reset-state, recent-unlocks
- Created AchievementIcon emoji mapper for all Lucide icon names
- Rewrote ViewModel with full state: scan tracking, recent unlocks,
  category/state filtering, rescan/reset actions with loading states
- Rewrote Screen: header stats row, scan status chip, stale indicator,
  category + state filter chips, recent unlocks section, expandable
  criteria, icon emoji, unlocked glow border, overflow menu with
  rescan/reset, reset confirmation dialog
- Added 30 string resources for all new UI elements

* fix: resolve CI compilation errors in achievements module

- Replace progress_pct references with correct Kotlin field progressPct (Achievement model uses @SerializedName for snake_case, Kotlin field is camelCase)

- Add missing onStart/onError parameters to safeLaunchLoad calls in loadScanStatus() and loadRecentUnlocks()

* fix: remove top padding on achievements tab content

* fix: remove redundant paddingValues on achievement LazyColumn

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-02 23:48:44 +03:00
1 parent 2bceac7ab3
commit f887146056
6 files changed
+941 -108

No files matched your search

@@ -1,7 +1,50 @@
package com.m57.hermescontrol.data.model
import com.google.gson.annotations.SerializedName
data class AchievementsResponse(
val achievements: List<Achievement>,
val achievements: List<Achievement>? = emptyList(),
@SerializedName("unlocked_count") val unlockedCount: Int = 0,
@SerializedName("discovered_count") val discoveredCount: Int = 0,
@SerializedName("secret_count") val secretCount: Int = 0,
@SerializedName("total_count") val totalCount: Int = 0,
@SerializedName("is_stale") val isStale: Boolean = false,
@SerializedName("generated_at") val generatedAt: Long? = null,
@SerializedName("scan_meta") val scanMeta: ScanMeta? = null,
val error: String? = null,
)
data class ScanMeta(
val status: ScanStatus? = null,
)
data class ScanStatus(
val state: String = "idle",
@SerializedName("started_at") val startedAt: Long? = null,
@SerializedName("finished_at") val finishedAt: Long? = null,
@SerializedName("last_error") val lastError: String? = null,
@SerializedName("last_duration_ms") val lastDurationMs: Long? = null,
@SerializedName("run_count") val runCount: Int = 0,
)
data class RecentUnlock(
val id: String,
val name: String,
val description: String?,
val category: String?,
val kind: String?,
val icon: String?,
val unlocked: Boolean,
val discovered: Boolean,
val state: String?,
val tier: String?,
val progress: Double?,
@SerializedName("next_tier") val nextTier: String?,
@SerializedName("next_threshold") val nextThreshold: Double?,
@SerializedName("progress_pct") val progressPct: Double?,
@SerializedName("unlocked_at") val unlockedAt: Long?,
val secret: Boolean?,
val criteria: String?,
)
data class Achievement(
@@ -16,7 +59,10 @@ data class Achievement(
val state: String?,
val tier: String?,
val progress: Double?,
val next_tier: String?,
val next_threshold: Double?,
val progress_pct: Double?,
@SerializedName("next_tier") val nextTier: String?,
@SerializedName("next_threshold") val nextThreshold: Double?,
@SerializedName("progress_pct") val progressPct: Double?,
@SerializedName("unlocked_at") val unlockedAt: Long?,
val secret: Boolean?,
val criteria: String?,
)
@@ -28,7 +28,9 @@ import com.m57.hermescontrol.data.model.PluginsHubResponse
import com.m57.hermescontrol.data.model.ProfileSoulResponse
import com.m57.hermescontrol.data.model.ProfilesResponse
import com.m57.hermescontrol.data.model.RawConfigResponse
import com.m57.hermescontrol.data.model.RecentUnlock
import com.m57.hermescontrol.data.model.SaveSkillContentRequest
import com.m57.hermescontrol.data.model.ScanStatus
import com.m57.hermescontrol.data.model.SessionListResponse
import com.m57.hermescontrol.data.model.SessionMessagesResponse
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
@@ -180,6 +182,18 @@ interface HermesApiService {
@GET("api/plugins/hermes-achievements/achievements")
suspend fun getAchievements(): Response<AchievementsResponse>
@GET("api/plugins/hermes-achievements/scan-status")
suspend fun getAchievementScanStatus(): Response<ScanStatus>
@POST("api/plugins/hermes-achievements/rescan")
suspend fun rescanAchievements(): Response<AchievementsResponse>
@POST("api/plugins/hermes-achievements/reset-state")
suspend fun resetAchievementState(): Response<Unit>
@GET("api/plugins/hermes-achievements/recent-unlocks")
suspend fun getRecentUnlocks(): Response<List<RecentUnlock>>
@GET("api/pairing")
suspend fun getPairing(): Response<PairingResponse>