* 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>
This commit is contained in:
1 parent
afa82ceec2
commit
9156064d2b
9 files changed
+1031
-1
No files matched your search
@@ -0,0 +1,125 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Usage analytics over a trailing window, returned by `GET /api/analytics/usage`.
|
||||
* Mirrors `web/src/lib/api.ts` `AnalyticsResponse`.
|
||||
*/
|
||||
@Serializable
|
||||
data class AnalyticsResponse(
|
||||
val daily: List<AnalyticsDailyEntry> = emptyList(),
|
||||
val by_model: List<AnalyticsModelEntry> = emptyList(),
|
||||
val totals: AnalyticsTotals = AnalyticsTotals(),
|
||||
val period_days: Int = 0,
|
||||
val skills: AnalyticsSkills = AnalyticsSkills(),
|
||||
val tools: List<AnalyticsToolUsage> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsToolUsage(
|
||||
val tool: String = "",
|
||||
val count: Int = 0,
|
||||
val percentage: Double = 0.0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsDailyEntry(
|
||||
val day: String = "",
|
||||
val input_tokens: Long = 0,
|
||||
val output_tokens: Long = 0,
|
||||
val cache_read_tokens: Long = 0,
|
||||
val reasoning_tokens: Long = 0,
|
||||
val estimated_cost: Double = 0.0,
|
||||
val actual_cost: Double = 0.0,
|
||||
val sessions: Int = 0,
|
||||
val api_calls: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsModelEntry(
|
||||
val model: String = "",
|
||||
val input_tokens: Long = 0,
|
||||
val output_tokens: Long = 0,
|
||||
val estimated_cost: Double = 0.0,
|
||||
val sessions: Int = 0,
|
||||
val api_calls: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsTotals(
|
||||
val total_input: Long = 0,
|
||||
val total_output: Long = 0,
|
||||
val total_cache_read: Long = 0,
|
||||
val total_reasoning: Long = 0,
|
||||
val total_estimated_cost: Double = 0.0,
|
||||
val total_actual_cost: Double = 0.0,
|
||||
val total_sessions: Int = 0,
|
||||
val total_api_calls: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsSkills(
|
||||
val summary: AnalyticsSkillsSummary = AnalyticsSkillsSummary(),
|
||||
val top_skills: List<AnalyticsSkillEntry> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsSkillEntry(
|
||||
val skill: String = "",
|
||||
val view_count: Int = 0,
|
||||
val manage_count: Int = 0,
|
||||
val total_count: Int = 0,
|
||||
val percentage: Double = 0.0,
|
||||
val last_used_at: Double? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnalyticsSkillsSummary(
|
||||
val total_skill_loads: Int = 0,
|
||||
val total_skill_edits: Int = 0,
|
||||
val total_skill_actions: Int = 0,
|
||||
val distinct_skills_used: Int = 0,
|
||||
)
|
||||
|
||||
/**
|
||||
* Per-model analytics over a trailing window, returned by
|
||||
* `GET /api/analytics/models`. Mirrors `web/src/lib/api.ts`
|
||||
* `ModelsAnalyticsResponse`.
|
||||
*/
|
||||
@Serializable
|
||||
data class ModelsAnalyticsResponse(
|
||||
val models: List<ModelsAnalyticsModelEntry> = emptyList(),
|
||||
val totals: ModelsAnalyticsTotals = ModelsAnalyticsTotals(),
|
||||
val period_days: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ModelsAnalyticsModelEntry(
|
||||
val model: String = "",
|
||||
val provider: String = "",
|
||||
val input_tokens: Long = 0,
|
||||
val output_tokens: Long = 0,
|
||||
val cache_read_tokens: Long = 0,
|
||||
val reasoning_tokens: Long = 0,
|
||||
val estimated_cost: Double = 0.0,
|
||||
val actual_cost: Double = 0.0,
|
||||
val sessions: Int = 0,
|
||||
val api_calls: Int = 0,
|
||||
val tool_calls: Int = 0,
|
||||
val last_used_at: Double? = null,
|
||||
val avg_tokens_per_session: Double = 0.0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ModelsAnalyticsTotals(
|
||||
val distinct_models: Int = 0,
|
||||
val total_input: Long = 0,
|
||||
val total_output: Long = 0,
|
||||
val total_cache_read: Long = 0,
|
||||
val total_reasoning: Long = 0,
|
||||
val total_estimated_cost: Double = 0.0,
|
||||
val total_actual_cost: Double = 0.0,
|
||||
val total_sessions: Int = 0,
|
||||
val total_api_calls: Int = 0,
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import com.m57.hermescontrol.data.model.ActionStatusResponse
|
||||
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
||||
import com.m57.hermescontrol.data.model.AddMcpServerRequest
|
||||
import com.m57.hermescontrol.data.model.AgentPluginInstallBody
|
||||
import com.m57.hermescontrol.data.model.AnalyticsResponse
|
||||
import com.m57.hermescontrol.data.model.AuxiliaryModelsResponse
|
||||
import com.m57.hermescontrol.data.model.BulkDeleteRequest
|
||||
import com.m57.hermescontrol.data.model.CheckpointsResponse
|
||||
@@ -45,6 +46,7 @@ import com.m57.hermescontrol.data.model.MoaConfigResponse
|
||||
import com.m57.hermescontrol.data.model.ModelAssignmentRequest
|
||||
import com.m57.hermescontrol.data.model.ModelAssignmentResponse
|
||||
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||
import com.m57.hermescontrol.data.model.ModelsAnalyticsResponse
|
||||
import com.m57.hermescontrol.data.model.PairingApproveRequest
|
||||
import com.m57.hermescontrol.data.model.PairingResponse
|
||||
import com.m57.hermescontrol.data.model.PairingRevokeRequest
|
||||
@@ -169,6 +171,18 @@ interface HermesApiService {
|
||||
@GET("api/system/stats")
|
||||
suspend fun getSystemStats(): Response<SystemStatsResponse>
|
||||
|
||||
@GET("api/analytics/usage")
|
||||
suspend fun getAnalytics(
|
||||
@Query("days") days: Int,
|
||||
@Query("profile") profile: String? = null,
|
||||
): Response<AnalyticsResponse>
|
||||
|
||||
@GET("api/analytics/models")
|
||||
suspend fun getModelsAnalytics(
|
||||
@Query("days") days: Int,
|
||||
@Query("profile") profile: String? = null,
|
||||
): Response<ModelsAnalyticsResponse>
|
||||
|
||||
@GET("api/skills")
|
||||
suspend fun getSkills(): Response<List<Skill>>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ data class CommandCatalog(
|
||||
val sub: Map<String, List<String>> = emptyMap(),
|
||||
val canon: Map<String, String> = emptyMap(),
|
||||
val categories: List<CommandCategory> = emptyList(),
|
||||
val skillCount: Int = 0,
|
||||
val skillCount: Double = 0.0,
|
||||
val warning: String = "",
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user